diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 82a17fc40c0..9ca1d946ab3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -17,6 +17,6 @@ ### Checklist - [ ] Test coverage for new or modified code paths -- [ ] Changelog is updated +- [ ] Changelog fragment(s) or "no changelog" label added (see [`changelog.d/README.md`](changelog.d/README.md)) - [ ] Required documentation changes (e.g., `docs/rpc/openapi.yaml` and `rpc-endpoints.md` for v2 endpoints, `event-dispatcher.md` for new events) - [ ] New clarity functions have corresponding PR in `clarity-benchmarking` repo diff --git a/.github/workflows/cargo-hack-check.yml b/.github/workflows/cargo-hack-check.yml index 31bfd4ac979..addfcc4e2fe 100644 --- a/.github/workflows/cargo-hack-check.yml +++ b/.github/workflows/cargo-hack-check.yml @@ -19,7 +19,7 @@ jobs: rust-toolchain: ${{ steps.toolchain.outputs.rust-toolchain }} steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -34,7 +34,7 @@ jobs: needs: setup steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -72,7 +72,7 @@ jobs: needs: setup steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -114,7 +114,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml new file mode 100644 index 00000000000..246eed89c1c --- /dev/null +++ b/.github/workflows/changelog-check.yml @@ -0,0 +1,70 @@ +name: Changelog Check + +on: + workflow_call: + +jobs: + changelog-check: + name: Check for changelog fragments + runs-on: ubuntu-latest + steps: + - name: Check for changelog fragments + uses: actions/github-script@v7 + with: + script: | + if (context.eventName !== 'pull_request') { + core.info(`Event is '${context.eventName}', not a pull request — skipping.`); + return; + } + // Fetch current labels (payload labels are stale on re-runs) + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + const labels = pr.labels.map(l => l.name); + if (labels.includes('no changelog')) { + core.info('PR has "no changelog" label — skipping changelog check.'); + return; + } + + const { data: files } = await github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + per_page: 100, + }); + + // Fail if CHANGELOG.md is modified directly + const directEdits = files.filter(f => + (f.filename === 'CHANGELOG.md' || f.filename === 'stacks-signer/CHANGELOG.md') && + f.status === 'modified' + ); + + if (directEdits.length > 0) { + const edited = directEdits.map(f => f.filename).join(', '); + core.setFailed( + `Do not edit ${edited} directly. ` + + 'Add a changelog fragment to changelog.d/ or stacks-signer/changelog.d/ instead ' + + '(see changelog.d/README.md for instructions).' + ); + return; + } + + const validExtensions = ['added', 'changed', 'fixed', 'removed']; + const fragments = files.filter(f => + (f.filename.startsWith('changelog.d/') || f.filename.startsWith('stacks-signer/changelog.d/')) && + f.status === 'added' && + validExtensions.some(ext => f.filename.endsWith(`.${ext}`)) + ); + + if (fragments.length === 0) { + core.setFailed( + 'No changelog fragment found. Please add a fragment file to changelog.d/ ' + + 'or stacks-signer/changelog.d/ (see changelog.d/README.md for instructions). ' + + 'If no changelog entry is needed, add the "no changelog" label to the PR.' + ); + } else { + const names = fragments.map(f => f.filename).join(', '); + core.info(`Found changelog fragment(s): ${names}`); + } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b91b747cbcf..865566aa1be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,10 @@ jobs: with: alias: "fmt-stacks" + changelog-check: + name: Changelog Check + uses: ./.github/workflows/changelog-check.yml + ###################################################################################### ## Check if the branch that this workflow is being run against is a release branch ## @@ -64,6 +68,7 @@ jobs: name: Check Release needs: - rustfmt + - changelog-check runs-on: ubuntu-latest outputs: node_tag: ${{ steps.check_release.outputs.node_tag }} @@ -96,6 +101,7 @@ jobs: name: Create Release needs: - rustfmt + - changelog-check - check-release secrets: inherit uses: ./.github/workflows/release-github.yml @@ -121,6 +127,7 @@ jobs: name: Create Test Cache needs: - rustfmt + - changelog-check - check-release uses: ./.github/workflows/create-cache.yml @@ -142,6 +149,7 @@ jobs: name: Stacks Core Tests needs: - rustfmt + - changelog-check - create-cache - check-release uses: ./.github/workflows/stacks-core-tests.yml @@ -164,6 +172,7 @@ jobs: name: Constants Check needs: - rustfmt + - changelog-check - check-release uses: ./.github/workflows/constants-check.yml @@ -185,6 +194,7 @@ jobs: name: Cargo Hack Check needs: - rustfmt + - changelog-check - check-release uses: ./.github/workflows/cargo-hack-check.yml @@ -205,6 +215,7 @@ jobs: name: Bitcoin Tests needs: - rustfmt + - changelog-check - create-cache - check-release uses: ./.github/workflows/bitcoin-tests.yml @@ -218,6 +229,7 @@ jobs: name: P2P Tests needs: - rustfmt + - changelog-check - create-cache - check-release uses: ./.github/workflows/p2p-tests.yml @@ -231,6 +243,51 @@ jobs: name: Epoch Tests needs: - rustfmt + - changelog-check - create-cache - check-release uses: ./.github/workflows/epoch-tests.yml + + ## Merge and upload code coverage report files once all tests are done + ## + ## Runs when: + ## - always (unless workflow is cancelled) + trigger-code-coverage-report: + if: ${{ always() && !cancelled() }} + name: Upload Code Coverage Report + runs-on: ubuntu-latest + needs: + - stacks-core-tests + - bitcoin-tests + - p2p-tests + - epoch-tests + steps: + # Checkout the code (Coveralls requires source code to be available when action is called) + - name: Checkout the latest code + id: git_checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + # Download the code coverage .info files generated by tests from artifacts (prefixed by commit SHA) + - name: Download code coverage artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # 8.0.1 + with: + pattern: ${{ github.sha }}-*.info + path: code_coverage_files + merge-multiple: true + # Install lcov for merging the reports + - name: Install lcov + shell: bash + run: | + sudo apt-get install -y --no-install-recommends lcov + # Merge n coverage report files into 1 file using lcov + - name: Merge code coverage files + shell: bash + run: | + cd code_coverage_files && \ + find . -name "*.info" | awk '{print "-a", $0}' | xargs lcov -o code-coverage-report.info + # Upload the merged code coverage file to Coveralls + - name: Upload code coverage to Coveralls + uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6 + with: + file: code_coverage_files/code-coverage-report.info + fail-on-error: true + \ No newline at end of file diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index ded23c7c6e3..a58fb893ca9 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout the latest code id: git_checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Define Rust Toolchain id: define_rust_toolchain diff --git a/.github/workflows/constants-check.yml b/.github/workflows/constants-check.yml index 28522fb6938..9c4a20274a6 100644 --- a/.github/workflows/constants-check.yml +++ b/.github/workflows/constants-check.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout the latest code id: git_checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Define Rust Toolchain id: define_rust_toolchain diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 025e22d9ea8..78a6b38d17c 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -57,7 +57,7 @@ jobs: ## Checkout the code - name: Checkout the latest code id: git_checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.BRANCH }} @@ -200,7 +200,7 @@ jobs: ## Checkout the code - name: Checkout the latest code id: git_checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.BRANCH }} sparse-checkout: | diff --git a/.github/workflows/fork-ci.yml b/.github/workflows/fork-ci.yml new file mode 100644 index 00000000000..d6822064daa --- /dev/null +++ b/.github/workflows/fork-ci.yml @@ -0,0 +1,116 @@ +## Lean CI for the stacks-core fork +## Runs fmt, clippy, and a build check on every push to fork/ branches +name: Fork CI + +on: + push: + branches: + - 'fork/**' + pull_request: + branches: + - 'fork/**' + +concurrency: + group: fork-ci-${{ github.ref || github.run_id }} + cancel-in-progress: true + +jobs: + rustfmt: + name: Format Check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: Check formatting + run: cargo fmt-stacks --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.lock') }} + + - name: Clippy (all crates except stackslib) + run: cargo clippy-stacks + + - name: Clippy (stackslib) + run: cargo clippy-stackslib + + build: + name: Build Check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }} + + - name: Build + run: cargo build --features monitoring_prom,slog_json --release + env: + CARGO_INCREMENTAL: 0 + + - name: Report binary size + run: | + ls -lh target/release/stacks-node + ls -lh target/release/stacks-signer + + test-quick: + name: Quick Tests + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }} + + - name: Install nextest + uses: taiki-e/install-action@nextest + + - name: Run tests (quick, non-ignored) + run: cargo nextest run --workspace --exclude stacks-node -j 4 + timeout-minutes: 60 + env: + RUST_BACKTRACE: 1 diff --git a/.github/workflows/nix-check.yml b/.github/workflows/nix-check.yml index 822b5b7cab1..13687340cb9 100644 --- a/.github/workflows/nix-check.yml +++ b/.github/workflows/nix-check.yml @@ -33,7 +33,7 @@ jobs: name: Check Nix package runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: DeterminateSystems/determinate-nix-action@d4b23d0b9eeeaeba3648c24d43bcb623dcf75336 # v3.7.0 - uses: DeterminateSystems/magic-nix-cache-action@e1c1dae8e170ed20fd2e6aaf9979ca2d3905d636 # v12 with: diff --git a/.github/workflows/proptest-extra-tests.yml b/.github/workflows/proptest-extra-tests.yml new file mode 100644 index 00000000000..1299551939b --- /dev/null +++ b/.github/workflows/proptest-extra-tests.yml @@ -0,0 +1,295 @@ +## GitHub workflow to run newly introduced proptests with higher case counts. +## +## This works for tests tagged with `t_prop` tag like this: +## +## #[tag(t_prop)] +## fn my_prop_test() { ... } +## +## Triggered by PR review events (submitted/dismissed) and manual dispatch. +## Manual dispatch always runs; PR-review runs proceed only for approved +## reviews targeting the develop base branch. +## +## It uses two gates: approval threshold first, then quick diff detection of +## new proptest tests (tag based), to avoid expensive runs when not needed. +## +## Discovery strategy: +## - HEAD: list tests from nextest archive (cache restore when available) +## - BASE: compile/list tests in a temporary git worktree +## - New tests: tagged tests present in HEAD but not in BASE + +name: Tests::Proptest Extra + +on: + pull_request_review: + types: [submitted, dismissed] + workflow_dispatch: + inputs: + base_ref: + description: "BASE branch to diff against (default: develop)" + required: false + default: "develop" + type: string + +defaults: + run: + shell: bash + +## Allow the job to be canceled when in progress due to new approval +concurrency: + group: proptest-extra-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +env: + RUST_BACKTRACE: full + SEGMENT_DOWNLOAD_TIMEOUT_MINS: 3 + TEST_TIMEOUT: 30 + ## Minimum number of PR approvals required before running the tests + REQUIRED_APPROVALS: 2 + ## Number of generated cases to run per selected proptest. + PROPTEST_CASES: 2500 + +jobs: + ## Gate 1: on manual dispatch, always proceed. + ## On pull_request_review, proceed only when the triggering review is an + ## approval targeting the `develop` base branch and check approvals + ## from users with write+ permission to be at least `REQUIRED_APPROVALS`. + check-approvals: + name: Check Approval Count + if: > + (github.event_name == 'workflow_dispatch') || + (github.event.review.state == 'approved' && github.event.pull_request.base.ref == 'develop') + runs-on: ubuntu-latest + outputs: + ready: ${{ steps.count.outputs.ready }} + steps: + - name: Count current approvals + id: count + env: + GH_TOKEN: ${{ github.token }} + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "workflow_dispatch — skipping approval count check" + echo "ready=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + ## Fetch all reviews and compute the latest state per reviewer. + ## A user who approved and then dismissed counts as dismissed (not approved). + approvers=$(gh api \ + "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews" \ + --jq ' + reduce .[] as $r ({}; + .[$r.user.login] = $r.state + ) + | to_entries + | map(select(.value == "APPROVED")) + | .[].key + ') + + ## Count only approvals from users with write+ permission. + count=0 + for user in $approvers; do + perm=$(gh api \ + "repos/${{ github.repository }}/collaborators/$user/permission" \ + --jq '.permission' 2>/dev/null || echo "none") + if [[ "$perm" == "write" || "$perm" == "maintain" || "$perm" == "admin" ]]; then + echo "Counting approval from $user (permission: $perm)" + count=$((count + 1)) + else + echo "Skipping approval from $user (permission: $perm)" + fi + done + + echo "Current approval count: $count (required: ${{ env.REQUIRED_APPROVALS }})" + + ## Run only when approvals reach REQUIRED_APPROVALS. + ## This helps avoid extra reruns triggered by later approvals + ## that would otherwise be queued by the concurrency setting. + if [ "$count" -ge "${{ env.REQUIRED_APPROVALS }}" ]; then + echo "ready=true" >> "$GITHUB_OUTPUT" + else + echo "ready=false" >> "$GITHUB_OUTPUT" + fi + + ## Gate 2: scan Git diff for newly added proptest tests (no compilation). + ## Skips the expensive test listing/filtering job when a PR does not + ## introduce any new proptest tests. + check-changes: + name: Detect New Proptest Tests + needs: check-approvals + if: needs.check-approvals.outputs.ready == 'true' + runs-on: ubuntu-latest + outputs: + found: ${{ steps.detect.outputs.found }} + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + ## Quick diff scan: detect whether the PR introduces any new proptest test + ## looking for the attribute #[tag(..., prop, ...)] on added lines, + ## so we can skip full discovery when there are none. + - name: Detect new proptest tests (git diff) + id: detect + env: + BASE_REF: ${{ github.event.pull_request.base.ref || inputs.base_ref }} + run: | + git fetch --depth=1 origin "$BASE_REF" + + found=false + ## For each .rs file that was added or modified in the PR... + while IFS= read -r file; do + ## ...check if any added line contains #[tag(..., t_prop, ...)] + ## The tag list may have multiple comma-separated keywords with optional spaces. + if git diff "origin/$BASE_REF" -- "$file" \ + | grep '^+[^+]' \ + | grep -qP '#\[tag\([^)]*\bt_prop\b[^)]*\)\]'; then + echo "Found new proptest tag in: $file" + found=true + break + fi + done < <(git diff "origin/$BASE_REF" --name-only --diff-filter=AM -- '*.rs') + + echo "found=$found" >> "$GITHUB_OUTPUT" + if [ "$found" = "true" ]; then + echo "New proptest tests detected — running full test discovery" + else + echo "No proptest tests detected — skipping test discovery" + fi + + proptests-run: + ## NOTE: This job name is used as a required Status Check in branch + ## protection rules. Renaming requires branch protection to be updated too. + ## When skipped via its `if:` condition, GitHub reports the status as + ## 'Success', so skipped runs are not blocking. + name: Run New Proptest Tests + needs: [check-approvals, check-changes] + if: | + needs.check-approvals.outputs.ready == 'true' && + needs.check-changes.outputs.found == 'true' + runs-on: ubuntu-latest + steps: + ## Setup test environment: try restoring `testenv` for HEAD. + ## Allows the job to proceed even if the cache restore fails. + ## The fallback steps will build the archive from source in that case. + - name: Setup Test Environment + id: setup_tests + continue-on-error: true + uses: stacks-network/actions/stacks-core/testenv@main + + ## If testenv failed (e.g. cache miss), cargo-nextest will not be installed. + ## Install it explicitly as a fallback using a pre-built binary. + - name: Install cargo-nextest (testenv fallback) + if: steps.setup_tests.outcome == 'failure' + uses: taiki-e/install-action@cargo-nextest + + ## If the nextest archive was not in cache, + ## build it from the HEAD source. + - name: Build HEAD nextest archive (testenv fallback) + if: steps.setup_tests.outcome == 'failure' + run: | + cargo nextest archive \ + --archive-file ~/test_archive.tar.zst \ + --workspace \ + --tests + + ## List every test present on the HEAD branch. + - name: List HEAD branch tests + run: | + cargo nextest list \ + --archive-file ~/test_archive.tar.zst \ + -Tjson 2>/dev/null \ + | jq -r ' + .["rust-suites"] + | to_entries[] + | .value.testcases + | keys[] + ' \ + | sort > /tmp/head-tests.txt + echo "HEAD tests: $(wc -l < /tmp/head-tests.txt)" + + ## Check out the base branch into a temporary git worktree and compile + ## it to list its tests. + ## + ## CARGO_TARGET_DIR is pointed at the HEAD workspace's target/ directory. + ## Anything unchanged between HEAD and BASE (external deps and + ## unmodified stacks-core crates) is reused, so only the crates + ## that actually differ need recompiling. + ## + ## NOTE: + ## Overwriting HEAD binaries in target/ is harmless — the test-run step + ## uses --archive-file, which extracts HEAD binaries into a temp dir and + ## never reads from target/. + ## + ## Reusing HEAD's target/ is only effective when testenv fell back to a local + ## build (cache miss): that build uses no special RUSTFLAGS, so the artifacts + ## are compatible and Rust only recompiles the crates that differ from BASE. + ## When testenv succeeds (cache hit), target/ contains artifacts built with + ## -Cinstrument-coverage (from create-cache). The flag mismatch invalidates + ## every fingerprint, forcing a full recompile regardless — no faster than + ## using a fresh target directory (and it not worthy using same flag either). + - name: List BASE branch tests + env: + BASE_REF: ${{ github.event.pull_request.base.ref || inputs.base_ref }} + run: | + echo "Base branch: $BASE_REF" + + git fetch --depth=1 origin "$BASE_REF" + git worktree add --detach /tmp/base-worktree "origin/$BASE_REF" + + ## Capture HEAD workspace root before entering the worktree + head_target="$(pwd)/target" + + pushd /tmp/base-worktree + CARGO_TARGET_DIR="$head_target" \ + cargo nextest list \ + -Tjson 2>/dev/null \ + | jq -r ' + .["rust-suites"] + | to_entries[] + | .value.testcases + | keys[] + ' \ + | sort > /tmp/base-tests.txt + popd + + ## Worktree source files are clean (build output went to HEAD_TARGET) + git worktree remove /tmp/base-worktree + git worktree prune + + echo "Base tests: $(wc -l < /tmp/base-tests.txt)" + + ## Compare the HEAD and base test lists and keep only tests that are new in HEAD. + ## Then filter to only those tagged with :t::...:t_prop:, i.e. test names of the form: + ## mod1::mod2::testname::t::t_prop::t + ## mod1::mod2::testname::t::other::t_prop::t + - name: Discover new tests + id: discover + run: | + comm -23 /tmp/head-tests.txt /tmp/base-tests.txt \ + | grep -P ':t::(?:.*::)?t_prop::' \ + > /tmp/new-prop-tests.txt || true + count=$(wc -l < /tmp/new-prop-tests.txt | tr -d ' ') + echo "count=$count" >> "$GITHUB_OUTPUT" + if [ "$count" -gt 0 ]; then + echo "New proptest tests found: $count" + cat /tmp/new-prop-tests.txt + else + echo "No new proptest tests discovered in this PR." + fi + + ## Run each new proptest test with an elevated `PROPTEST_CASES` value. + ## The step fails if any individual test fails. + - name: Run new tests (PROPTEST_CASES=${{ env.PROPTEST_CASES }}) + if: steps.discover.outputs.count != '0' + timeout-minutes: ${{ fromJSON(env.TEST_TIMEOUT) }} + run: | + while IFS= read -r test_name || [[ -n "$test_name" ]]; do + [[ -z "$test_name" ]] && continue + echo "::group::$test_name" + cargo nextest run \ + --archive-file ~/test_archive.tar.zst \ + -E "test(=$test_name)" + echo "::endgroup::" + done < /tmp/new-prop-tests.txt diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 0baa8d09504..c3293c4f70a 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -73,7 +73,7 @@ jobs: ## Checkout the code - name: Checkout the latest code id: git_checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.ref }} diff --git a/.github/workflows/release-docker.yml b/.github/workflows/release-docker.yml index 00d2a4da464..8e884287fbd 100644 --- a/.github/workflows/release-docker.yml +++ b/.github/workflows/release-docker.yml @@ -112,7 +112,7 @@ jobs: ## Checkout the code - name: Checkout the latest code id: git_checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.BRANCH_NAME }} sparse-checkout: | diff --git a/.github/workflows/stacks-core-tests.yml b/.github/workflows/stacks-core-tests.yml index 8612c7e7fbd..e9e9303c229 100644 --- a/.github/workflows/stacks-core-tests.yml +++ b/.github/workflows/stacks-core-tests.yml @@ -91,7 +91,7 @@ jobs: ## checkout the code - name: Checkout the latest code id: git_checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run network relay tests id: nettest @@ -109,7 +109,7 @@ jobs: steps: - name: Checkout the latest code id: git_checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Execute core contract unit tests with clarinet-sdk id: clarinet_unit_test uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 @@ -119,15 +119,6 @@ jobs: cache-dependency-path: "./contrib/core-contract-tests/package-lock.json" - run: npm ci - run: npm test - ## Upload code coverage file - - name: Code Coverage - id: codecov - uses: stacks-network/actions/codecov@main - with: - # We'd like to uncomment the below line once the codecov upload is working - # fail_ci_if_error: true - test-name: ${{ github.job }} - upload-only: true # Core contract tests on Clarinet v1 # Check for false positives/negatives @@ -137,7 +128,7 @@ jobs: steps: - name: Checkout the latest code id: git_checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Execute core contract unit tests in Clarinet id: clarinet_unit_test_v1 uses: docker://hirosystems/clarinet:1.7.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cd3007edce..7bd1ab6b62c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to the versioning scheme outlined in the [README.md](README.md). +## [3.4.0.0.0] + +### Added + +- Set the epoch 3.4 activation height to 943,333 per SIP-039 and finalize all settings for epoch 3.4 and Clarity version 5. +- Added post-condition enhancements for epoch 3.4 (SIP-040): `Originator` post-condition mode (`0x03`) and NFT `MAY SEND` condition code (`0x12`), including serialization support and epoch-gated validation/enforcement. +- Disabled `at-block` starting from Epoch 3.4 (see SIP-042). New contracts referencing `at-block` are rejected during static analysis. Existing contracts that invoke it will fail at runtime with an `AtBlockUnavailable` error. + +### Changed + +- `/v3/blocks/simulate/{block_id}` and `/v3/block/replay ` no longer emit transaction events for post condition aborted transactions. +- `EventDispatcher` no longer emits transaction events for post condition aborted transactions. + ## [3.3.0.0.6] ### Added @@ -24,6 +37,10 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE - Signer protocol version negotiation now properly handles downgrades based on majority consensus, not just upgrades - The sortition DB now tracks canonical Stacks tip by its burn view, allowing it to recover from a chain freeze if the Bitcoin block upon which the ongoing tenure is based is orphened before the last tenure block is processed. +### Changed + +- `EventDispatcher` no longer emits transaction events for post condition aborted transactions. + ## [3.3.0.0.5] ### Added diff --git a/Cargo.toml b/Cargo.toml index b54aa38aa13..d11d0c775c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,8 @@ opt-level = 1 opt-level = 3 [profile.release] -debug = true +debug = false +strip = true codegen-units = 1 lto = "fat" diff --git a/Dockerfile b/Dockerfile index ca03fa3ac60..08708250c23 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,5 +12,8 @@ RUN cargo build --features monitoring_prom,slog_json --release RUN cp -R target/release/. /out FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* COPY --from=build /out/stacks-node /out/stacks-signer /out/stacks-inspect /bin/ +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD curl -sf http://localhost:20443/v2/info > /dev/null || exit 1 CMD ["stacks-node", "mainnet"] diff --git a/README.md b/README.md index 9f8f38c4d13..1f9a3b2c63b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Stacks is a layer-2 blockchain that uses Bitcoin as a base layer for security an [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg?style=flat)](https://www.gnu.org/licenses/gpl-3.0) [![Release](https://img.shields.io/github/v/release/stacks-network/stacks-core?style=flat)](https://github.com/stacks-network/stacks-core/releases/latest) [![Discord Chat](https://img.shields.io/discord/621759717756370964.svg)](https://stacks.chat) +[![Coverage Status](https://coveralls.io/repos/github/stacks-network/stacks-core/badge.svg)](https://coveralls.io/github/stacks-network/stacks-core) ## Building diff --git a/changelog.d/7000-genesis-sync-stuck.fixed b/changelog.d/7000-genesis-sync-stuck.fixed new file mode 100644 index 00000000000..3d55ac0352e --- /dev/null +++ b/changelog.d/7000-genesis-sync-stuck.fixed @@ -0,0 +1 @@ +Fix a bug that could cause genesis sync to stall forever due to a slow path in computing the canonical Stacks tip getting triggerred when the Stacks tip is significantly behind the sortition tip. diff --git a/changelog.d/README.md b/changelog.d/README.md new file mode 100644 index 00000000000..8312dfdf556 --- /dev/null +++ b/changelog.d/README.md @@ -0,0 +1,35 @@ +# Changelog Fragments + +Instead of editing `CHANGELOG.md` directly, each PR should add a **fragment +file** to this directory. This avoids merge conflicts and makes the release +process clearer. + +## How to add a changelog entry + +1. Create a file in this directory named: `-.` + + **Categories:** `added`, `changed`, `fixed`, `removed` + + **Examples:** + - `6811-marf-compress.added` + - `6744-tenure-mining-fix.fixed` + - `6900-remove-deprecated-rpc.removed` + +2. Write the changelog entry text in the file (one or more lines of markdown): + + ``` + Added `marf_compress` as a node configuration parameter to enable MARF compression feature ([#6811](https://github.com/stacks-network/stacks-core/pull/6811)) + ``` + +3. That's it. The fragment will be assembled into `CHANGELOG.md` at release time + using `contrib/assemble-changelog.sh`. + +## Notes + +- One fragment per PR is typical, but you can add multiple if your PR spans + categories. +- If your PR doesn't need a changelog entry (e.g., docs-only, CI changes, + test-only), you can skip this. Add the `no changelog` label to your PR to + bypass the CI check. +- Fragment files are deleted after they are assembled into the changelog during + a release. diff --git a/clarity-types/src/errors/analysis.rs b/clarity-types/src/errors/analysis.rs index 64e5095df8e..2a6f76347c8 100644 --- a/clarity-types/src/errors/analysis.rs +++ b/clarity-types/src/errors/analysis.rs @@ -21,7 +21,7 @@ use crate::diagnostic::{DiagnosableError, Diagnostic}; use crate::errors::{ClarityTypeError, CostErrors}; use crate::execution_cost::ExecutionCost; use crate::representations::SymbolicExpression; -use crate::types::{TraitIdentifier, TupleTypeSignature, TypeSignature, Value}; +use crate::types::{TraitIdentifier, TupleTypeSignature, TypeSignature}; /// What kind of syntax binding was found to be in error? #[derive(Debug, PartialEq, Clone, Copy)] @@ -498,6 +498,8 @@ pub enum StaticCheckErrorKind { WriteAttemptedInReadOnly, /// `at-block` closure must be read-only but contains write operations. AtBlockClosureMustBeReadOnly, + /// `at-block` is not available in this epoch. + AtBlockUnavailable, // contract post-conditions /// Post-condition expects a list of asset allowances but received invalid input. @@ -567,17 +569,19 @@ pub enum RuntimeCheckErrorKind { /// The first `Box` wraps the expected type, and the second wraps the actual type. TypeError(Box, Box), /// Value does not match the expected type during type-checking. - /// The `Box` wraps the expected type, and the `Box` wraps the invalid value. - TypeValueError(Box, Box), + /// The `Box` wraps the expected type, and the `String` is a + /// truncated display representation of the invalid value. + TypeValueError(Box, String), // Union type mismatch /// Value does not belong to the expected union of types during type-checking. - /// The `Vec` represents the expected types, and the `Box` wraps the invalid value. - UnionTypeValueError(Vec, Box), + /// The `Vec` represents the expected types, and the `String` is a + /// truncated display representation of the invalid value. + UnionTypeValueError(Vec, String), /// Expected a contract principal value but found a different value. - /// The `Box` wraps the actual value provided. - ExpectedContractPrincipalValue(Box), + /// The `String` is a truncated display representation of the actual value provided. + ExpectedContractPrincipalValue(String), // Match type errors /// Could not determine the type of an expression during analysis. @@ -609,6 +613,8 @@ pub enum RuntimeCheckErrorKind { /// Referenced function is not defined in the current scope. /// The `String` wraps the non-existent function name. UndefinedFunction(String), + /// `at-block` is not available in this epoch. + AtBlockUnavailable, // Argument counts /// Incorrect number of arguments provided to a function. @@ -785,7 +791,9 @@ impl From for RuntimeCheckErrorKind { ClarityTypeError::TypeSignatureTooDeep => Self::TypeSignatureTooDeep, ClarityTypeError::ValueOutOfBounds => Self::ValueOutOfBounds, ClarityTypeError::DuplicateTupleField(name) => Self::NameAlreadyUsed(name), - ClarityTypeError::TypeMismatchValue(ty, value) => Self::TypeValueError(ty, value), + ClarityTypeError::TypeMismatchValue(ty, value) => { + Self::TypeValueError(ty, value.to_error_string()) + } ClarityTypeError::TypeMismatch(expected, found) => Self::TypeError(expected, found), ClarityTypeError::ListTypeMismatch => Self::ListTypesMustMatch, ClarityTypeError::InvalidAsciiCharacter(_) => Self::InvalidCharactersDetected, @@ -1181,6 +1189,7 @@ impl DiagnosableError for StaticCheckErrorKind { StaticCheckErrorKind::TooManyFunctionParameters(found, allowed) => format!("too many function parameters specified: found {found}, the maximum is {allowed}"), StaticCheckErrorKind::WriteAttemptedInReadOnly => "expecting read-only statements, detected a writing operation".into(), StaticCheckErrorKind::AtBlockClosureMustBeReadOnly => "(at-block ...) closures expect read-only statements, but detected a writing operation".into(), + StaticCheckErrorKind::AtBlockUnavailable => "(at-block ...) is not available in this epoch".into(), StaticCheckErrorKind::BadTokenName => "expecting an token name as an argument".into(), StaticCheckErrorKind::DefineNFTBadSignature => "(define-asset ...) expects an asset name and an asset identifier type signature as arguments".into(), StaticCheckErrorKind::NoSuchNFT(asset_name) => format!("tried to use asset function with a undefined asset ('{asset_name}')"), diff --git a/clarity-types/src/tests/types/signatures.rs b/clarity-types/src/tests/types/signatures.rs index 0e789cfa8f7..5c29e2ad6fa 100644 --- a/clarity-types/src/tests/types/signatures.rs +++ b/clarity-types/src/tests/types/signatures.rs @@ -1027,58 +1027,3 @@ fn test_least_supertype() { ); } } - -#[test] -fn test_callable_principal_admits_identical_callable_principal_epoch34() { - let contract_id = QualifiedContractIdentifier::local("foo").unwrap(); - let callable_ty = TypeSignature::CallableType(CallableSubtype::Principal(contract_id)); - - assert!( - callable_ty - .admits_type(&StacksEpochId::Epoch34, &callable_ty) - .unwrap() - ); -} - -#[test] -fn test_callable_principal_rejects_different_callable_principal_epoch34() { - let contract_a = QualifiedContractIdentifier::local("foo").unwrap(); - let contract_b = QualifiedContractIdentifier::local("bar").unwrap(); - let callable_a = TypeSignature::CallableType(CallableSubtype::Principal(contract_a)); - let callable_b = TypeSignature::CallableType(CallableSubtype::Principal(contract_b)); - - assert!( - !callable_a - .admits_type(&StacksEpochId::Epoch34, &callable_b) - .unwrap() - ); -} - -#[test] -fn test_callable_principal_rejects_plain_principal_type_epoch34() { - let contract_id = QualifiedContractIdentifier::local("foo").unwrap(); - let callable_ty = TypeSignature::CallableType(CallableSubtype::Principal(contract_id)); - - assert!( - !callable_ty - .admits_type(&StacksEpochId::Epoch34, &TypeSignature::PrincipalType) - .unwrap() - ); -} - -#[test] -fn test_sanitize_value_accepts_callable_principal_value_epoch34() { - let contract_id = QualifiedContractIdentifier::local("foo").unwrap(); - let expected = TypeSignature::CallableType(CallableSubtype::Principal(contract_id.clone())); - let value = Value::CallableContract(CallableData { - contract_identifier: contract_id, - trait_identifier: None, - }); - - let (sanitized, did_sanitize) = - Value::sanitize_value(&StacksEpochId::Epoch34, &expected, value.clone()) - .expect("callable principal should sanitize successfully"); - - assert_eq!(sanitized, value); - assert!(!did_sanitize); -} diff --git a/clarity-types/src/types/mod.rs b/clarity-types/src/types/mod.rs index efb5db50313..11537433a2a 100644 --- a/clarity-types/src/types/mod.rs +++ b/clarity-types/src/types/mod.rs @@ -57,6 +57,8 @@ pub const MAX_TO_ASCII_BUFFER_LEN: u32 = (MAX_TO_ASCII_RESULT_LEN - 2) / 2; pub const MAX_TYPE_DEPTH: u8 = 32; /// this is the charged size for wrapped values, i.e., response or optionals pub const WRAPPER_VALUE_SIZE: u32 = 1; +/// Maximum byte length for Value string representations in error messages. +const MAX_ERROR_VALUE_DISPLAY_LEN: usize = 512; #[derive(Debug, Clone, Eq, Serialize, Deserialize)] pub struct TupleData { @@ -310,7 +312,7 @@ impl TraitIdentifier { } } -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Value { Int(i128), UInt(u128), @@ -1345,6 +1347,21 @@ impl Value { )) } } + + /// Format as a truncated string for use in error messages. + /// Avoids cloning potentially large Values in error paths. + pub fn to_error_string(&self) -> String { + let full = format!("{self:?}"); + if full.len() <= MAX_ERROR_VALUE_DISPLAY_LEN { + full + } else { + let end = (0..=MAX_ERROR_VALUE_DISPLAY_LEN) + .rev() + .find(|&i| full.is_char_boundary(i)) + .unwrap_or(0); + format!("{}...", &full[..end]) + } + } } impl BuffData { diff --git a/clarity-types/src/types/signatures.rs b/clarity-types/src/types/signatures.rs index 54c2dad23c8..8a9df8c0936 100644 --- a/clarity-types/src/types/signatures.rs +++ b/clarity-types/src/types/signatures.rs @@ -553,16 +553,6 @@ impl TypeSignature { } fn admits_type_v2_1(&self, other: &TypeSignature) -> Result { - // Callable principal types are runtime-carried in epoch 3.4+, and should - // admit an identical callable principal type directly (before concretization). - if let ( - CallableType(CallableSubtype::Principal(self_contract)), - CallableType(CallableSubtype::Principal(other_contract)), - ) = (self, other) - { - return Ok(self_contract == other_contract); - } - let other = match other.concretize() { Ok(other) => other, Err(_) => { diff --git a/clarity/Cargo.toml b/clarity/Cargo.toml index 06448cc302c..24dca0eb69c 100644 --- a/clarity/Cargo.toml +++ b/clarity/Cargo.toml @@ -44,6 +44,10 @@ criterion = "0.8.2" name = "sequence_ops" harness = false +[[bench]] +name = "value_ref" +harness = false + [target.'cfg(not(target_family = "wasm"))'.dependencies] serde_stacker = "0.1" diff --git a/clarity/benches/value_ref.rs b/clarity/benches/value_ref.rs new file mode 100644 index 00000000000..c5c4e848f2b --- /dev/null +++ b/clarity/benches/value_ref.rs @@ -0,0 +1,214 @@ +// Copyright (C) 2026 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Benchmarks for the ValueRef zero-copy variable lookup change. +//! +//! Compares Epoch33 (old: `lookup_variable` always clones + sanitizes) against +//! Epoch34 (new: `lookup_variable` returns a borrowed reference for pre-sanitized +//! epochs, deferring or eliminating the clone entirely). +//! +//! Three scenarios exercise the primary beneficiaries: +//! +//! 1. `fold_buf_cmp` — fold over a list where each step does `(>= BIG-BUF BIG-BUF)`. +//! Each step looks up a 128-byte contract constant twice. `special_geq_v2` uses +//! `as_ref()` throughout, so Epoch34 allocates nothing for the operands. +//! +//! 2. `fold_ascii_cmp` — same pattern with a 128-char ASCII string constant. +//! +//! 3. `let_local_refs` — a `let` that binds a 128-byte buffer to `x`, then references +//! `x` N times via `(>= x 0x00)`. Shows the local-variable lookup benefit. + +use std::hint::black_box; + +use clarity::vm::contexts::{ContractContext, GlobalContext}; +use clarity::vm::costs::LimitedCostTracker; +use clarity::vm::database::MemoryBackingStore; +use clarity::vm::representations::SymbolicExpression; +use clarity::vm::types::QualifiedContractIdentifier; +use clarity::vm::version::ClarityVersion; +use clarity::vm::{ast, eval_all}; +use criterion::{BatchSize, BenchmarkId, Criterion, criterion_group, criterion_main}; +use stacks_common::consts::CHAIN_ID_TESTNET; +use stacks_common::types::StacksEpochId; + +const VERSION: ClarityVersion = ClarityVersion::Clarity2; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn parse(source: &str, epoch: StacksEpochId) -> Vec { + let contract_id = QualifiedContractIdentifier::transient(); + let mut cost = LimitedCostTracker::new_free(); + ast::build_ast(&contract_id, source, &mut cost, VERSION, epoch) + .expect("failed to parse benchmark program") + .expressions +} + +/// Execute `parsed` in a fresh environment for `epoch`. +/// `marf` is provided by the caller (created in `iter_batched` setup) so that +/// SQLite initialisation is excluded from the timing window. +fn run(parsed: &[SymbolicExpression], epoch: StacksEpochId, mut marf: MemoryBackingStore) { + let contract_id = QualifiedContractIdentifier::transient(); + let db = marf.as_clarity_db(); + let mut global_context = GlobalContext::new( + false, + CHAIN_ID_TESTNET, + db, + LimitedCostTracker::new_free(), + epoch, + ); + let mut ctx = ContractContext::new(contract_id, VERSION); + black_box( + global_context + .execute(|g| eval_all(parsed, &mut ctx, g, None)) + .unwrap(), + ); +} + +// --------------------------------------------------------------------------- +// Program generators +// --------------------------------------------------------------------------- + +/// `(fold cmp-step (list 1 … steps) true)` where `cmp-step` does +/// `(>= BIG-BUF BIG-BUF)` — 2 contract-constant lookups per step. +fn make_fold_buf_program(steps: usize) -> String { + let buf_hex = "ab".repeat(128); // 128-byte buffer + let list_elems = (1..=steps) + .map(|i| i.to_string()) + .collect::>() + .join(" "); + format!( + r#" +(define-constant BIG-BUF 0x{buf_hex}) +(define-private (cmp-step (i int) (acc bool)) + (>= BIG-BUF BIG-BUF)) +(fold cmp-step (list {list_elems}) true)"# + ) +} + +/// Same as above but with a 128-char ASCII string constant. +fn make_fold_ascii_program(steps: usize) -> String { + let str_content = "a".repeat(128); + let list_elems = (1..=steps) + .map(|i| i.to_string()) + .collect::>() + .join(" "); + format!( + r#" +(define-constant BIG-STR "{str_content}") +(define-private (cmp-step (i int) (acc bool)) + (>= BIG-STR BIG-STR)) +(fold cmp-step (list {list_elems}) true)"# + ) +} + +/// `(let ((x BIG-BUF)) (and (>= x 0x00) … refs times …))` +/// Measures lookup of a local context variable `refs` times. +fn make_let_local_program(refs: usize) -> String { + let buf_hex = "ab".repeat(128); + let comparisons = (0..refs) + .map(|_| "(>= x 0x00)".to_string()) + .collect::>() + .join(" "); + format!( + r#" +(define-constant BIG-BUF 0x{buf_hex}) +(let ((x BIG-BUF)) + (and {comparisons}))"# + ) +} + +// --------------------------------------------------------------------------- +// Benchmark groups +// --------------------------------------------------------------------------- + +fn bench_fold_buf(c: &mut Criterion) { + let mut group = c.benchmark_group("value_ref/fold_buf_cmp"); + for &steps in &[50usize, 200] { + let program = make_fold_buf_program(steps); + let parsed_33 = parse(&program, StacksEpochId::Epoch33); + let parsed_34 = parse(&program, StacksEpochId::Epoch34); + + group.bench_function(BenchmarkId::new("epoch33", steps), |b| { + b.iter_batched( + MemoryBackingStore::new, + |marf| run(&parsed_33, StacksEpochId::Epoch33, marf), + BatchSize::SmallInput, + ); + }); + group.bench_function(BenchmarkId::new("epoch34", steps), |b| { + b.iter_batched( + MemoryBackingStore::new, + |marf| run(&parsed_34, StacksEpochId::Epoch34, marf), + BatchSize::SmallInput, + ); + }); + } + group.finish(); +} + +fn bench_fold_ascii(c: &mut Criterion) { + let mut group = c.benchmark_group("value_ref/fold_ascii_cmp"); + for &steps in &[50usize, 200] { + let program = make_fold_ascii_program(steps); + let parsed_33 = parse(&program, StacksEpochId::Epoch33); + let parsed_34 = parse(&program, StacksEpochId::Epoch34); + + group.bench_function(BenchmarkId::new("epoch33", steps), |b| { + b.iter_batched( + MemoryBackingStore::new, + |marf| run(&parsed_33, StacksEpochId::Epoch33, marf), + BatchSize::SmallInput, + ); + }); + group.bench_function(BenchmarkId::new("epoch34", steps), |b| { + b.iter_batched( + MemoryBackingStore::new, + |marf| run(&parsed_34, StacksEpochId::Epoch34, marf), + BatchSize::SmallInput, + ); + }); + } + group.finish(); +} + +fn bench_let_local(c: &mut Criterion) { + let mut group = c.benchmark_group("value_ref/let_local_refs"); + for &refs in &[10usize, 50] { + let program = make_let_local_program(refs); + let parsed_33 = parse(&program, StacksEpochId::Epoch33); + let parsed_34 = parse(&program, StacksEpochId::Epoch34); + + group.bench_function(BenchmarkId::new("epoch33", refs), |b| { + b.iter_batched( + MemoryBackingStore::new, + |marf| run(&parsed_33, StacksEpochId::Epoch33, marf), + BatchSize::SmallInput, + ); + }); + group.bench_function(BenchmarkId::new("epoch34", refs), |b| { + b.iter_batched( + MemoryBackingStore::new, + |marf| run(&parsed_34, StacksEpochId::Epoch34, marf), + BatchSize::SmallInput, + ); + }); + } + group.finish(); +} + +criterion_group!(benches, bench_fold_buf, bench_fold_ascii, bench_let_local); +criterion_main!(benches); diff --git a/clarity/src/vm/analysis/read_only_checker/tests.rs b/clarity/src/vm/analysis/read_only_checker/tests.rs index f17eac0d627..3b2a3770844 100644 --- a/clarity/src/vm/analysis/read_only_checker/tests.rs +++ b/clarity/src/vm/analysis/read_only_checker/tests.rs @@ -21,36 +21,42 @@ use rstest_reuse::{self, *}; use stacks_common::types::StacksEpochId; use crate::vm::ClarityVersion; -use crate::vm::analysis::type_check; -use crate::vm::analysis::type_checker::v2_1::tests::mem_type_check; +use crate::vm::analysis::{mem_type_check as mem_run_analysis, type_check}; use crate::vm::ast::parse; use crate::vm::database::MemoryBackingStore; use crate::vm::errors::StaticCheckErrorKind; +use crate::vm::functions::NativeFunctions; use crate::vm::tests::test_clarity_versions; use crate::vm::types::QualifiedContractIdentifier; -#[test] -fn test_argument_count_violations() { - let examples = [ - ( - "(define-private (foo-bar) - (at-block))", - StaticCheckErrorKind::IncorrectArgumentCount(2, 0), - ), - ( - "(define-private (foo-bar) (map-get?))", - StaticCheckErrorKind::IncorrectArgumentCount(2, 0), - ), - ]; +/// Helper: returns true if `at-block` is available in the given clarity version. +fn has_at_block(version: &ClarityVersion) -> bool { + NativeFunctions::lookup_by_name_at_version("at-block", version).is_some() +} + +#[apply(test_clarity_versions)] +fn test_argument_count_violations(#[case] version: ClarityVersion, #[case] epoch: StacksEpochId) { + // map-get? is available in all versions + let err = + mem_run_analysis("(define-private (foo-bar) (map-get?))", version, epoch).unwrap_err(); + assert_eq!(*err.err, StaticCheckErrorKind::IncorrectArgumentCount(2, 0)); - for (contract, expected) in examples.iter() { - let err = mem_type_check(contract).unwrap_err(); - assert_eq!(*err.err, *expected) + // at-block is removed in Clarity 5 + let at_block_contract = "(define-private (foo-bar) + (at-block))"; + let err = mem_run_analysis(at_block_contract, version, epoch).unwrap_err(); + if has_at_block(&version) { + assert_eq!(*err.err, StaticCheckErrorKind::IncorrectArgumentCount(2, 0)); + } else { + assert_eq!( + *err.err, + StaticCheckErrorKind::UnknownFunction("at-block".into()) + ); } } -#[test] -fn test_at_block_violations() { +#[apply(test_clarity_versions)] +fn test_at_block_violations(#[case] version: ClarityVersion, #[case] epoch: StacksEpochId) { let examples = [ "(define-data-var foo int 1) (define-private (foo-bar) @@ -71,14 +77,30 @@ fn test_at_block_violations() { ]; for contract in examples.iter() { - let err = mem_type_check(contract).unwrap_err(); - eprintln!("{err}"); - assert_eq!(*err.err, StaticCheckErrorKind::AtBlockClosureMustBeReadOnly) + let err = mem_run_analysis(contract, version, epoch).unwrap_err(); + if has_at_block(&version) { + assert_eq!( + *err.err, + StaticCheckErrorKind::AtBlockClosureMustBeReadOnly, + "Expected AtBlockClosureMustBeReadOnly in {version}/{epoch}" + ); + } else { + assert_eq!( + *err.err, + StaticCheckErrorKind::UnknownFunction("at-block".into()), + "Expected UnknownFunction for at-block in {version}/{epoch}" + ); + } } } -#[test] -fn test_simple_read_only_violations() { +#[apply(test_clarity_versions)] +fn test_simple_read_only_violations(#[case] version: ClarityVersion, #[case] epoch: StacksEpochId) { + // replace-at? is only available in Clarity 2+ + if version < ClarityVersion::Clarity2 { + return; + } + // note -- these examples have _type errors_ in addition to read-only errors, // but the read only error should end up taking precedence let bad_contracts = [ @@ -163,13 +185,13 @@ fn test_simple_read_only_violations() { ]; for contract in bad_contracts.iter() { - let err = mem_type_check(contract).unwrap_err(); + let err = mem_run_analysis(contract, version, epoch).unwrap_err(); assert_eq!(*err.err, StaticCheckErrorKind::WriteAttemptedInReadOnly) } } -#[test] -fn test_nested_writing_closure() { +#[apply(test_clarity_versions)] +fn test_nested_writing_closure(#[case] version: ClarityVersion, #[case] epoch: StacksEpochId) { let bad_contracts = ["(define-data-var cursor int 0) (define-public (bad-at-block-function) (begin @@ -180,8 +202,20 @@ fn test_nested_writing_closure() { (ok 1)))"]; for contract in bad_contracts.iter() { - let err = mem_type_check(contract).unwrap_err(); - assert_eq!(*err.err, StaticCheckErrorKind::AtBlockClosureMustBeReadOnly) + let err = mem_run_analysis(contract, version, epoch).unwrap_err(); + if has_at_block(&version) { + assert_eq!( + *err.err, + StaticCheckErrorKind::AtBlockClosureMustBeReadOnly, + "Expected AtBlockClosureMustBeReadOnly in {version}/{epoch}" + ); + } else { + assert_eq!( + *err.err, + StaticCheckErrorKind::UnknownFunction("at-block".into()), + "Expected UnknownFunction for at-block in {version}/{epoch}" + ); + } } } diff --git a/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs b/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs index 0c7d4bdbec2..30347869868 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs @@ -132,6 +132,9 @@ fn check_special_at_block( context: &TypingContext, ) -> Result { check_argument_count(2, args)?; + if !checker.epoch.supports_at_block() { + return Err(StaticCheckErrorKind::AtBlockUnavailable.into()); + } checker.type_check_expects(&args[0], context, &TypeSignature::BUFFER_32)?; checker.type_check(&args[1], context) } diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs index 8311a4515b9..bd4cb2b7cee 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs @@ -2641,6 +2641,38 @@ fn clarity_trait_experiments_downcast_literal_3( assert!(err.starts_with("TraitReferenceUnknown(\"p\")")); } +/// A constant defined as `(if cond .contract-a .contract-b)` — both branches +/// are literal contract principals, so the set of possible targets is +/// statically known. The type checker currently rejects this because the `if` +/// expression types to `PrincipalType` rather than `CallableType`. +/// Future work: accept this case, since all branches are known at analysis time. +#[apply(test_clarity_versions)] +fn clarity_trait_experiments_downcast_literal_4( + #[case] version: ClarityVersion, + #[case] epoch: StacksEpochId, +) { + let mut marf = MemoryBackingStore::new(); + let mut db = marf.as_analysis_db(); + + let err = db + .execute(|db| { + load_versioned(db, "math-trait", version, epoch)?; + load_versioned(db, "impl-math-trait", version, epoch)?; + load_versioned(db, "downcast-literal-4", version, epoch) + }) + .unwrap_err(); + match version { + ClarityVersion::Clarity1 => { + assert!(err.starts_with("TraitReferenceUnknown(\"target\")")); + } + _ => { + // TODO: future type checker enhancement should accept this case, + // since both if-branches are statically-known contract principals. + assert!(err.starts_with("ExpectedCallableType(PrincipalType)")); + } + } +} + #[apply(test_clarity_versions)] fn clarity_trait_experiments_downcast_trait_2( #[case] version: ClarityVersion, diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts/downcast-literal-4.clar b/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts/downcast-literal-4.clar new file mode 100644 index 00000000000..cc38d2e081c --- /dev/null +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts/downcast-literal-4.clar @@ -0,0 +1,12 @@ +;; A constant whose value is a conditional over literal contract principals. +;; The type checker currently rejects this with ExpectedCallableType(PrincipalType) +;; because the `if` expression types to PrincipalType, not CallableType. +;; Future work: the type checker should accept this, since all branches are +;; statically-known contract principals. + +(define-constant use-impl-a true) +(define-constant target (if use-impl-a .impl-math-trait .impl-math-trait)) + +(define-public (call-add) + (contract-call? target add u1 u2) +) diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs index fa28bd9cc0f..17c1d77651c 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs @@ -927,12 +927,51 @@ fn test_at_block() { for (good_test, expected) in good.iter() { assert_eq!( expected, - &format!("{}", type_check_helper(good_test).unwrap()) + &format!( + "{}", + type_check_helper_version( + good_test, + ClarityVersion::Clarity4, + StacksEpochId::Epoch33 + ) + .unwrap() + ) ); } for (bad_test, expected) in bad.iter() { - assert_eq!(*expected, *type_check_helper(bad_test).unwrap_err().err); + assert_eq!( + *expected, + *type_check_helper_version(bad_test, ClarityVersion::Clarity4, StacksEpochId::Epoch33) + .unwrap_err() + .err + ); + } + + assert_eq!( + StaticCheckErrorKind::AtBlockUnavailable, + *type_check_helper_version( + "(at-block (sha256 u0) u1)", + ClarityVersion::Clarity4, + StacksEpochId::Epoch34 + ) + .unwrap_err() + .err + ); + + let mut versions_gt_clarity4 = ClarityVersion::ALL.to_vec(); + versions_gt_clarity4.retain(|version| *version > ClarityVersion::Clarity4); + for version in versions_gt_clarity4 { + assert_eq!( + StaticCheckErrorKind::UnknownFunction("at-block".to_string()), + *type_check_helper_version( + "(at-block (sha256 u0) u1)", + version, + StacksEpochId::latest() + ) + .unwrap_err() + .err + ); } } diff --git a/clarity/src/vm/callables.rs b/clarity/src/vm/callables.rs index 7ce5a6cbc5d..76d9021018c 100644 --- a/clarity/src/vm/callables.rs +++ b/clarity/src/vm/callables.rs @@ -24,7 +24,7 @@ use super::ClarityVersion; use super::costs::{CostErrors, CostOverflowingMath}; use super::errors::VmInternalError; use super::types::signatures::CallableSubtype; -use crate::vm::contexts::ContractContext; +use crate::vm::contexts::{ContractContext, ExecutionState, InvocationContext}; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::runtime_cost; use crate::vm::errors::{RuntimeCheckErrorKind, VmExecutionError, check_argument_count}; @@ -33,7 +33,7 @@ use crate::vm::types::{ CallableData, ListData, ListTypeData, OptionalData, PrincipalData, ResponseData, SequenceData, SequenceSubtype, TraitIdentifier, TupleData, TypeSignature, }; -use crate::vm::{Environment, LocalContext, Value, eval}; +use crate::vm::{LocalContext, Value, eval}; #[allow(clippy::type_complexity, clippy::large_enum_variant)] pub enum CallableType { @@ -52,7 +52,8 @@ pub enum CallableType { &'static str, &'static dyn Fn( &[SymbolicExpression], - &mut Environment, + &mut ExecutionState, + &InvocationContext, &LocalContext, ) -> Result, ), @@ -83,14 +84,21 @@ pub enum NativeHandle { DoubleArg(&'static dyn Fn(Value, Value) -> Result), MoreArg(&'static dyn Fn(Vec) -> Result), #[allow(clippy::type_complexity)] - MoreArgEnv(&'static dyn Fn(Vec, &mut Environment) -> Result), + MoreArgEnv( + &'static dyn Fn( + Vec, + &mut ExecutionState, + &InvocationContext, + ) -> Result, + ), } impl NativeHandle { pub fn apply( &self, mut args: Vec, - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, ) -> Result { match self { Self::SingleArg(function) => { @@ -111,7 +119,7 @@ impl NativeHandle { function(first, second) } Self::MoreArg(function) => function(args), - Self::MoreArgEnv(function) => function(args, env), + Self::MoreArgEnv(function) => function(args, exec_state, invoke_ctx), } } } @@ -150,23 +158,28 @@ impl DefinedFunction { pub fn execute_apply( &self, args: &[Value], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, ) -> Result { runtime_cost( ClarityCostFunction::UserFunctionApplication, - env, + exec_state, self.arguments.len(), )?; - if env.epoch().uses_arg_size_for_cost() { + if exec_state.epoch().uses_arg_size_for_cost() { for arg in args.iter() { - runtime_cost(ClarityCostFunction::InnerTypeCheckCost, env, arg.size()?)?; + runtime_cost( + ClarityCostFunction::InnerTypeCheckCost, + exec_state, + arg.size()?, + )?; } } else { for arg_type in self.arg_types.iter() { runtime_cost( ClarityCostFunction::InnerTypeCheckCost, - env, + exec_state, arg_type.size()?, )?; } @@ -191,13 +204,13 @@ impl DefinedFunction { let ((name, type_sig), value) = arg; // Clarity 1 behavior - if *env.contract_context.get_clarity_version() < ClarityVersion::Clarity2 { + if *invoke_ctx.contract_context.get_clarity_version() < ClarityVersion::Clarity2 { match (type_sig, value) { // Epoch < 2.1 uses TraitReferenceType ( TypeSignature::TraitReferenceType(trait_identifier), Value::Principal(PrincipalData::Contract(callee_contract_id)), - ) if *env.epoch() < StacksEpochId::Epoch21 => { + ) if *exec_state.epoch() < StacksEpochId::Epoch21 => { // Argument is a trait reference, probably leading to a dynamic contract call // We keep a reference of the mapping (var-name: (callee_contract_id, trait_id)) in the context. // The code fetching and checking the trait is implemented in the contract_call eval function. @@ -213,7 +226,7 @@ impl DefinedFunction { ( TypeSignature::CallableType(CallableSubtype::Trait(trait_identifier)), Value::Principal(PrincipalData::Contract(callee_contract_id)), - ) if *env.epoch() >= StacksEpochId::Epoch21 => { + ) if *exec_state.epoch() >= StacksEpochId::Epoch21 => { // Argument is a trait reference, probably leading to a dynamic contract call // We keep a reference of the mapping (var-name: (callee_contract_id, trait_id)) in the context. // The code fetching and checking the trait is implemented in the contract_call eval function. @@ -244,10 +257,10 @@ impl DefinedFunction { ); } _ => { - if !type_sig.admits(env.epoch(), value)? { + if !type_sig.admits(exec_state.epoch(), value)? { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(type_sig.clone()), - Box::new(value.clone()), + value.to_error_string(), ) .into()); } @@ -291,10 +304,10 @@ impl DefinedFunction { ); } _ => { - if !type_sig.admits(env.epoch(), &cast_value)? { + if !type_sig.admits(exec_state.epoch(), &cast_value)? { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(type_sig.clone()), - Box::new(cast_value), + cast_value.to_error_string(), ) .into()); } @@ -307,12 +320,12 @@ impl DefinedFunction { } } - let result = eval(&self.body, env, &context); + let result = eval(&self.body, exec_state, invoke_ctx, &context); // if the error wasn't actually an error, but a function return, // pull that out and return it. match result { - Ok(r) => Ok(r), + Ok(r) => Ok(r.clone_with_cost(exec_state)?), Err(e) => match e { VmExecutionError::EarlyReturn(v) => Ok(v.into()), _ => Err(e), @@ -356,11 +369,20 @@ impl DefinedFunction { self.define_type == DefineType::ReadOnly } - pub fn apply(&self, args: &[Value], env: &mut Environment) -> Result { + pub fn apply( + &self, + args: &[Value], + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, + ) -> Result { match self.define_type { - DefineType::Private => self.execute_apply(args, env), - DefineType::Public => env.execute_function_as_transaction(self, args, None, false), - DefineType::ReadOnly => env.execute_function_as_transaction(self, args, None, false), + DefineType::Private => self.execute_apply(args, exec_state, invoke_ctx), + DefineType::Public => { + exec_state.execute_function_as_transaction(invoke_ctx, self, args, None, false) + } + DefineType::ReadOnly => { + exec_state.execute_function_as_transaction(invoke_ctx, self, args, None, false) + } } } @@ -478,7 +500,7 @@ fn clarity2_implicit_cast( // This should be unreachable if the type-checker has already run successfully return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(type_sig.clone()), - Box::new(value.clone()), + value.to_error_string(), ) .into()); } diff --git a/clarity/src/vm/clarity.rs b/clarity/src/vm/clarity.rs index ed91a4cd7f6..d07861b2905 100644 --- a/clarity/src/vm/clarity.rs +++ b/clarity/src/vm/clarity.rs @@ -22,7 +22,7 @@ use crate::vm::analysis::{ }; use crate::vm::ast::ContractAST; use crate::vm::ast::errors::{ParseError, ParseErrorKind}; -use crate::vm::contexts::{AssetMap, Environment, OwnedEnvironment}; +use crate::vm::contexts::{AssetMap, ExecutionState, InvocationContext, OwnedEnvironment}; use crate::vm::costs::{ExecutionCost, LimitedCostTracker}; use crate::vm::database::ClarityDatabase; use crate::vm::errors::{ClarityEvalError, VmExecutionError}; @@ -206,7 +206,7 @@ pub trait ClarityConnection { to_do: F, ) -> Result where - F: FnOnce(&mut Environment) -> Result, + F: FnOnce(&mut ExecutionState, &InvocationContext) -> Result, { let epoch_id = self.get_epoch(); let clarity_version = ClarityVersion::default_for_epoch(epoch_id); diff --git a/clarity/src/vm/contexts.rs b/clarity/src/vm/contexts.rs index c50d5413fd8..825c8f7f387 100644 --- a/clarity/src/vm/contexts.rs +++ b/clarity/src/vm/contexts.rs @@ -48,31 +48,90 @@ use crate::vm::types::{ TraitIdentifier, TypeSignature, Value, }; use crate::vm::version::ClarityVersion; -use crate::vm::{ast, eval, is_reserved, stx_transfer_consolidated}; +use crate::vm::{ValueRef, ast, eval, is_reserved, stx_transfer_consolidated}; pub const MAX_CONTEXT_DEPTH: u64 = 256; pub const MAX_EVENTS_BATCH: u64 = 50 * 1024 * 1024; -// TODO: -// hide the environment's instance variables. -// we don't want many of these changing after instantiation. -/// Environments pack a reference to the global context (which is basically the db), -/// the current contract context, a call stack, the current sender, caller, and -/// sponsor (if one exists). -/// Essentially, the point of the Environment struct is to prevent all the eval functions -/// from including all of these items in their method signatures individually. Because -/// these different contexts can be mixed and matched (i.e., in a contract-call, you change -/// contract context), a single "invocation" will end up creating multiple environment -/// objects as context changes occur. -pub struct Environment<'a, 'b, 'hooks> { - pub global_context: &'a mut GlobalContext<'b, 'hooks>, +/// Immutable metadata describing a single contract invocation. +/// +/// `InvocationContext` captures *who* is executing *which contract* under what authority. +/// It contains the principals that define call semantics (`sender`, `caller`, `sponsor`) +/// together with the active `ContractContext`. +/// +/// A new `InvocationContext` is derived whenever a contract call changes authority +/// (e.g., `contract-call?`, `as-contract`, or sponsor propagation). It is intentionally +/// immutable so that nested calls cannot mutate the caller's view of authority. +/// +/// This type does **not** contain mutable VM state (database, cost tracker, events, stack). +/// Those live in [`ExecutionState`]. Lexical variables and scope live in [`LocalContext`]. +/// +/// Together: +/// - `InvocationContext` → authority + contract binding +/// - `ExecutionState` → mutable runtime state +/// - `LocalContext` → lexical variables/scope +pub struct InvocationContext<'a> { + /// The contract currently being executed. pub contract_context: &'a ContractContext, - pub call_stack: &'a mut CallStack, + /// The transaction sender for this invocation (tx origin or `as-contract` principal). pub sender: Option, + /// The immediate caller of the current contract (may differ from `sender` in nested calls). pub caller: Option, + /// The sponsor responsible for paying execution costs, if any. pub sponsor: Option, } +impl InvocationContext<'_> { + /// Returns a derived invocation context executing *as* the given principal. + /// + /// Both `sender` and `caller` are set to `sender` + /// The sponsor and contract context are preserved. + pub fn with_principal(&self, sender: PrincipalData) -> Self { + InvocationContext { + contract_context: self.contract_context, + sender: Some(sender.clone()), + caller: Some(sender), + sponsor: self.sponsor.clone(), + } + } + + /// Returns a derived invocation context with a different immediate caller. + /// + /// This models a nested contract call where authority flows from the same + /// transaction sender but the caller changes to the calling contract. + /// The sender, sponsor, and contract context are preserved. + pub fn with_caller(&self, caller: PrincipalData) -> Self { + InvocationContext { + contract_context: self.contract_context, + sender: self.sender.clone(), + caller: Some(caller), + sponsor: self.sponsor.clone(), + } + } +} + +/// `ExecutionState` contains the parts of the VM environment that may change during +/// evaluation: the global chainstate (`GlobalContext`) and the Clarity call stack. +/// All database writes, event emission, cost tracking, and stack mutations occur +/// through this structure. +/// +/// Unlike [`InvocationContext`], this state is shared and mutated throughout the +/// lifetime of a single invocation. Nested contract or function calls reborrow the +/// same `ExecutionState` while deriving new `InvocationContext` and/or `LocalContext` +/// values. +/// +/// Separation of concerns: +/// - `ExecutionState` → mutable VM/runtime state +/// - `InvocationContext` → authority + contract binding +/// - `LocalContext` → lexical variables/scope +pub struct ExecutionState<'a, 'b, 'hooks> { + /// Global chainstate and database access for this execution. + pub global_context: &'a mut GlobalContext<'b, 'hooks>, + + /// The Clarity call stack tracking nested function/contract calls. + pub call_stack: &'a mut CallStack, +} + pub struct OwnedEnvironment<'a, 'hooks> { pub(crate) context: GlobalContext<'a, 'hooks>, call_stack: CallStack, @@ -263,6 +322,11 @@ pub struct ContractContext { pub data_size: u64, /// The clarity version of this contract clarity_version: ClarityVersion, + /// True while the contract is being deployed (inside `initialize_from_ast`). + /// Constants may only be used as `contract-call?` dispatch targets + /// after deployment, when their values are frozen. + #[serde(skip)] + pub is_deploying: bool, } pub struct LocalContext<'a> { @@ -380,14 +444,14 @@ impl AssetMap { &mut self, principal: &PrincipalData, asset: AssetIdentifier, - transfered: Value, + transferred: Value, ) { let principal_map = self.asset_map.entry(principal.clone()).or_default(); if let Some(map_entry) = principal_map.get_mut(&asset) { - map_entry.push(transfered); + map_entry.push(transferred); } else { - principal_map.insert(asset, vec![transfered]); + principal_map.insert(asset, vec![transferred]); } } @@ -679,14 +743,18 @@ impl<'a, 'hooks> OwnedEnvironment<'a, 'hooks> { sender: Option, sponsor: Option, context: &'b ContractContext, - ) -> Environment<'b, 'a, 'hooks> { - Environment::new( - &mut self.context, - context, - &mut self.call_stack, - sender.clone(), - sender, - sponsor, + ) -> (ExecutionState<'b, 'a, 'hooks>, InvocationContext<'b>) { + ( + ExecutionState { + global_context: &mut self.context, + call_stack: &mut self.call_stack, + }, + InvocationContext { + contract_context: context, + sender: sender.clone(), + caller: sender, + sponsor, + }, ) } @@ -699,7 +767,7 @@ impl<'a, 'hooks> OwnedEnvironment<'a, 'hooks> { ) -> std::result::Result<(A, AssetMap, Vec), E> where E: From, - F: FnOnce(&mut Environment) -> std::result::Result, + F: FnOnce(&mut ExecutionState, &InvocationContext) -> std::result::Result, { assert!(self.context.is_top_level()); self.begin(); @@ -709,8 +777,9 @@ impl<'a, 'hooks> OwnedEnvironment<'a, 'hooks> { QualifiedContractIdentifier::transient(), ClarityVersion::Clarity1, )); - let mut exec_env = self.get_exec_environment(Some(sender), sponsor, &initial_context); - f(&mut exec_env) + let (mut exec_state, invoke_ctx) = + self.get_exec_environment(Some(sender), sponsor, &initial_context); + f(&mut exec_state, &invoke_ctx) }; match result { @@ -738,7 +807,9 @@ impl<'a, 'hooks> OwnedEnvironment<'a, 'hooks> { contract_identifier.issuer.clone().into(), sponsor, None, - |exec_env| exec_env.initialize_contract(contract_identifier, contract_content), + |exec_state, invoke_ctx| { + exec_state.initialize_contract(invoke_ctx, contract_identifier, contract_content) + }, ) } @@ -756,7 +827,9 @@ impl<'a, 'hooks> OwnedEnvironment<'a, 'hooks> { QualifiedContractIdentifier::transient(), version, )), - |exec_env| exec_env.initialize_contract(contract_identifier, contract_content), + |exec_state, invoke_ctx| { + exec_state.initialize_contract(invoke_ctx, contract_identifier, contract_content) + }, ) } @@ -775,8 +848,9 @@ impl<'a, 'hooks> OwnedEnvironment<'a, 'hooks> { QualifiedContractIdentifier::transient(), clarity_version, )), - |exec_env| { - exec_env.initialize_contract_from_ast( + |exec_state, invoke_ctx| { + exec_state.initialize_contract_from_ast( + invoke_ctx, contract_identifier, clarity_version, contract_content, @@ -794,8 +868,8 @@ impl<'a, 'hooks> OwnedEnvironment<'a, 'hooks> { tx_name: &str, args: &[SymbolicExpression], ) -> Result<(Value, AssetMap, Vec), VmExecutionError> { - self.execute_in_env(sender, sponsor, None, |exec_env| { - exec_env.execute_contract(&contract_identifier, tx_name, args, false) + self.execute_in_env(sender, sponsor, None, |exec_state, invoke_ctx| { + exec_state.execute_contract(invoke_ctx, &contract_identifier, tx_name, args, false) }) } @@ -806,8 +880,8 @@ impl<'a, 'hooks> OwnedEnvironment<'a, 'hooks> { amount: u128, memo: &BuffData, ) -> Result<(Value, AssetMap, Vec), VmExecutionError> { - self.execute_in_env(from.clone(), None, None, |exec_env| { - exec_env.stx_transfer(from, to, amount, memo) + self.execute_in_env(from.clone(), None, None, |exec_state, invoke_ctx| { + exec_state.stx_transfer(invoke_ctx, from, to, amount, memo) }) } @@ -817,24 +891,30 @@ impl<'a, 'hooks> OwnedEnvironment<'a, 'hooks> { #[cfg(any(test, feature = "testing"))] pub fn stx_faucet(&mut self, recipient: &PrincipalData, amount: u128) { - self.execute_in_env::<_, _, VmExecutionError>(recipient.clone(), None, None, |env| { - let mut snapshot = env - .global_context - .database - .get_stx_balance_snapshot(recipient) - .unwrap(); + self.execute_in_env::<_, _, VmExecutionError>( + recipient.clone(), + None, + None, + |exec_state, _invoke_ctx| { + let mut snapshot = exec_state + .global_context + .database + .get_stx_balance_snapshot(recipient) + .unwrap(); - snapshot.credit(amount).unwrap(); - snapshot.save().unwrap(); + snapshot.credit(amount).unwrap(); + snapshot.save().unwrap(); - env.global_context - .database - .increment_ustx_liquid_supply(amount) - .unwrap(); + exec_state + .global_context + .database + .increment_ustx_liquid_supply(amount) + .unwrap(); - let res: std::result::Result<(), VmExecutionError> = Ok(()); - res - }) + let res: std::result::Result<(), VmExecutionError> = Ok(()); + res + }, + ) .unwrap(); } @@ -847,7 +927,7 @@ impl<'a, 'hooks> OwnedEnvironment<'a, 'hooks> { QualifiedContractIdentifier::transient().issuer.into(), None, None, - |exec_env| exec_env.eval_raw(program), + |exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, program), ) } @@ -860,7 +940,7 @@ impl<'a, 'hooks> OwnedEnvironment<'a, 'hooks> { QualifiedContractIdentifier::transient().issuer.into(), None, None, - |exec_env| exec_env.eval_read_only(contract, program), + |exec_state, invoke_ctx| exec_state.eval_read_only(invoke_ctx, contract, program), ) } @@ -902,7 +982,7 @@ impl<'a, 'hooks> OwnedEnvironment<'a, 'hooks> { } } -impl CostTracker for Environment<'_, '_, '_> { +impl CostTracker for ExecutionState<'_, '_, '_> { fn compute_cost( &mut self, cost_function: ClarityCostFunction, @@ -968,65 +1048,31 @@ impl CostTracker for GlobalContext<'_, '_> { } } -impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { - /// Returns an Environment value & checks the types of the contract sender, caller, and sponsor - /// - /// # Panics - /// Panics if the Value types for sender (Principal), caller (Principal), or sponsor - /// (Optional Principal) are incorrect. - pub fn new( - global_context: &'a mut GlobalContext<'b, 'hooks>, - contract_context: &'a ContractContext, - call_stack: &'a mut CallStack, - sender: Option, - caller: Option, - sponsor: Option, - ) -> Environment<'a, 'b, 'hooks> { - Environment { - global_context, - contract_context, - call_stack, - sender, - caller, - sponsor, - } - } - - /// Leaving sponsor value as is for this new context (as opposed to setting it to None) - pub fn nest_as_principal<'c>( - &'c mut self, - sender: PrincipalData, - ) -> Environment<'c, 'b, 'hooks> { - Environment::new( - self.global_context, - self.contract_context, - self.call_stack, - Some(sender.clone()), - Some(sender), - self.sponsor.clone(), - ) - } - - pub fn nest_with_caller<'c>( - &'c mut self, - caller: PrincipalData, - ) -> Environment<'c, 'b, 'hooks> { - Environment::new( - self.global_context, - self.contract_context, - self.call_stack, - self.sender.clone(), - Some(caller), - self.sponsor.clone(), - ) +impl<'a, 'b, 'hooks> ExecutionState<'a, 'b, 'hooks> { + /// Used only for contract-call! cost short-circuiting. Once the short-circuited cost + /// has been evaluated and assessed, the contract-call! itself is executed "for free". + pub fn run_free(&mut self, invoke_ctx: &InvocationContext, to_run: F) -> A + where + F: FnOnce(&mut ExecutionState, &InvocationContext) -> A, + { + let original_tracker = replace( + &mut self.global_context.cost_track, + LimitedCostTracker::new_free(), + ); + // note: it is important that this method not return until original_tracker has been + // restored. DO NOT use the try syntax (?). + let result = to_run(self, invoke_ctx); + self.global_context.cost_track = original_tracker; + result } pub fn eval_read_only( &mut self, + invoke_ctx: &InvocationContext, contract_identifier: &QualifiedContractIdentifier, program: &str, ) -> Result { - let parsed = self.parse_nonempty_program(contract_identifier, program)?; + let parsed = self.parse_nonempty_program(invoke_ctx, contract_identifier, program)?; self.global_context.begin(); @@ -1040,46 +1086,36 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { })?; let result = { - let mut nested_env = Environment::new( - self.global_context, - &contract.contract_context, - self.call_stack, - self.sender.clone(), - self.caller.clone(), - self.sponsor.clone(), - ); + let nested_view = InvocationContext { + contract_context: &contract.contract_context, + sender: invoke_ctx.sender.clone(), + caller: invoke_ctx.caller.clone(), + sponsor: invoke_ctx.sponsor.clone(), + }; let local_context = LocalContext::new(); - eval(&parsed[0], &mut nested_env, &local_context) + eval(&parsed[0], self, &nested_view, &local_context) + .and_then(|value| value.clone_with_cost(self)) } .map_err(ClarityEvalError::from); self.global_context.roll_back()?; - result } - pub fn eval_raw(&mut self, program: &str) -> Result { - let parsed = - self.parse_nonempty_program(&QualifiedContractIdentifier::transient(), program)?; + pub fn eval_raw( + &mut self, + invoke_ctx: &InvocationContext, + program: &str, + ) -> Result { + let parsed = self.parse_nonempty_program( + invoke_ctx, + &QualifiedContractIdentifier::transient(), + program, + )?; let local_context = LocalContext::new(); - eval(&parsed[0], self, &local_context).map_err(ClarityEvalError::from) - } - - /// Used only for contract-call! cost short-circuiting. Once the short-circuited cost - /// has been evaluated and assessed, the contract-call! itself is executed "for free". - pub fn run_free(&mut self, to_run: F) -> A - where - F: FnOnce(&mut Environment) -> A, - { - let original_tracker = replace( - &mut self.global_context.cost_track, - LimitedCostTracker::new_free(), - ); - // note: it is important that this method not return until original_tracker has been - // restored. DO NOT use the try syntax (?). - let result = to_run(self); - self.global_context.cost_track = original_tracker; - result + eval(&parsed[0], self, invoke_ctx, &local_context) + .and_then(|value| value.clone_with_cost(self)) + .map_err(ClarityEvalError::from) } /// Parse `program` into a **non-empty** list of `SymbolicExpression`s. @@ -1098,6 +1134,7 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { /// callers that bypass normal validation paths. fn parse_nonempty_program( &mut self, + invoke_ctx: &InvocationContext, contract_identifier: &QualifiedContractIdentifier, program: &str, ) -> Result, ClarityEvalError> { @@ -1105,7 +1142,7 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { contract_identifier, program, self, - self.contract_context.clarity_version, + invoke_ctx.contract_context.clarity_version, self.global_context.epoch_id, )? .expressions; @@ -1128,12 +1165,13 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { pub fn execute_contract( &mut self, + invoke_ctx: &InvocationContext, contract: &QualifiedContractIdentifier, tx_name: &str, args: &[SymbolicExpression], read_only: bool, ) -> Result { - self.inner_execute_contract(contract, tx_name, args, read_only, false) + self.inner_execute_contract(invoke_ctx, contract, tx_name, args, read_only, false) } /// This method is exposed for callers that need to invoke a private method directly. @@ -1141,12 +1179,13 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { /// on the pox-2 contract. This should not be called by user transaction processing. pub fn execute_contract_allow_private( &mut self, + invoke_ctx: &InvocationContext, contract: &QualifiedContractIdentifier, tx_name: &str, args: &[SymbolicExpression], read_only: bool, ) -> Result { - self.inner_execute_contract(contract, tx_name, args, read_only, true) + self.inner_execute_contract(invoke_ctx, contract, tx_name, args, read_only, true) } /// This method handles actual execution of contract-calls on a contract. @@ -1157,6 +1196,7 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { /// `Environment::execute_contract_allow_private`. fn inner_execute_contract( &mut self, + invoke_ctx: &InvocationContext, contract_identifier: &QualifiedContractIdentifier, tx_name: &str, args: &[SymbolicExpression], @@ -1195,7 +1235,7 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { value.clone(), ).ok_or_else(|| RuntimeCheckErrorKind::TypeValueError( Box::new(expected_type), - Box::new(value.clone()), + value.to_error_string(), ) )?; @@ -1210,7 +1250,7 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { return Err(RuntimeCheckErrorKind::CircularReference(vec![func_identifier.to_string()]).into()) } self.call_stack.insert(&func_identifier, true); - let res = self.execute_function_as_transaction(&func, &args, Some(&contract.contract_context), allow_private); + let res = self.execute_function_as_transaction(invoke_ctx, &func, &args, Some(&contract.contract_context), allow_private); self.call_stack.remove(&func_identifier, true)?; match res { @@ -1218,8 +1258,8 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { if let Some(handler) = self.global_context.database.get_cc_special_cases_handler() { handler( self.global_context, - self.sender.as_ref(), - self.sponsor.as_ref(), + invoke_ctx.sender.as_ref(), + invoke_ctx.sponsor.as_ref(), contract_identifier, tx_name, &args, @@ -1235,6 +1275,7 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { pub fn execute_function_as_transaction( &mut self, + invoke_ctx: &InvocationContext, function: &DefinedFunction, args: &[Value], next_contract_context: Option<&ContractContext>, @@ -1248,19 +1289,16 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { self.global_context.begin(); } - let next_contract_context = next_contract_context.unwrap_or(self.contract_context); + let next_contract_context = next_contract_context.unwrap_or(invoke_ctx.contract_context); let result = { - let mut nested_env = Environment::new( - self.global_context, - next_contract_context, - self.call_stack, - self.sender.clone(), - self.caller.clone(), - self.sponsor.clone(), - ); - - function.execute_apply(args, &mut nested_env) + let nested_view = InvocationContext { + contract_context: next_contract_context, + sender: invoke_ctx.sender.clone(), + caller: invoke_ctx.caller.clone(), + sponsor: invoke_ctx.sponsor.clone(), + }; + function.execute_apply(args, self, &nested_view) }; if make_read_only { @@ -1271,12 +1309,13 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { } } - pub fn evaluate_at_block( + pub fn evaluate_at_block<'e>( &mut self, bhh: StacksBlockId, - closure: &SymbolicExpression, - local: &LocalContext, - ) -> Result { + closure: &'e SymbolicExpression, + invoke_ctx: &'e InvocationContext, + local: &'e LocalContext, + ) -> Result, VmExecutionError> { self.global_context.begin_read_only(); let result = self @@ -1284,7 +1323,7 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { .database .set_block_hash(bhh, false) .and_then(|prior_bhh| { - let result = eval(closure, self, local); + let result = eval(closure, self, invoke_ctx, local); self.global_context .database .set_block_hash(prior_bhh, true) @@ -1303,10 +1342,11 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { pub fn initialize_contract( &mut self, + invoke_ctx: &InvocationContext, contract_identifier: QualifiedContractIdentifier, contract_content: &str, ) -> Result<(), ClarityEvalError> { - let clarity_version = self.contract_context.clarity_version; + let clarity_version = invoke_ctx.contract_context.clarity_version; let contract_ast = ast::build_ast( &contract_identifier, @@ -1316,6 +1356,7 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { self.global_context.epoch_id, )?; self.initialize_contract_from_ast( + invoke_ctx, contract_identifier, clarity_version, &contract_ast, @@ -1326,6 +1367,7 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { pub fn initialize_contract_from_ast( &mut self, + invoke_ctx: &InvocationContext, contract_identifier: QualifiedContractIdentifier, contract_version: ClarityVersion, contract_content: &ContractAST, @@ -1365,7 +1407,7 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { let result = Contract::initialize_from_ast( contract_identifier.clone(), contract_content, - self.sponsor.clone(), + invoke_ctx.sponsor.clone(), self.global_context, contract_version, ); @@ -1399,13 +1441,14 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { /// (miners should never build blocks that spend non-existent STX in a top-level token-transfer) pub fn stx_transfer( &mut self, + invoke_ctx: &InvocationContext, from: &PrincipalData, to: &PrincipalData, amount: u128, memo: &BuffData, ) -> Result { self.global_context.begin(); - let result = stx_transfer_consolidated(self, from, to, amount, memo); + let result = stx_transfer_consolidated(self, invoke_ctx, from, to, amount, memo); match result { Ok(value) => match value .clone() @@ -1428,13 +1471,17 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { } } - pub fn run_as_transaction(&mut self, f: F) -> std::result::Result + pub fn run_as_transaction( + &mut self, + invoke_ctx: &InvocationContext, + f: F, + ) -> std::result::Result where - F: FnOnce(&mut Self) -> std::result::Result, + F: FnOnce(&mut Self, &InvocationContext) -> std::result::Result, E: From, { self.global_context.begin(); - let result = f(self); + let result = f(self, invoke_ctx); match result { Ok(ret) => { self.global_context.commit()?; @@ -1472,21 +1519,25 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { } pub fn construct_print_transaction_event( - contract_id: &QualifiedContractIdentifier, - value: &Value, + contract_id: QualifiedContractIdentifier, + value: Value, ) -> StacksTransactionEvent { let print_event = SmartContractEventData { - key: (contract_id.clone(), "print".to_string()), - value: value.clone(), + key: (contract_id, "print".to_string()), + value, }; StacksTransactionEvent::SmartContractEvent(print_event) } - pub fn register_print_event(&mut self, value: Value) -> Result<(), VmExecutionError> { + pub fn register_print_event( + &mut self, + invoke_ctx: &InvocationContext, + value: Value, + ) -> Result<(), VmExecutionError> { let event = Self::construct_print_transaction_event( - &self.contract_context.contract_identifier, - &value, + invoke_ctx.contract_context.contract_identifier.clone(), + value, ); self.push_to_event_batch(event)?; @@ -1682,14 +1733,14 @@ impl<'a, 'hooks> GlobalContext<'a, 'hooks> { sender: &PrincipalData, contract_identifier: &QualifiedContractIdentifier, asset_name: &ClarityName, - transfered: Value, + transferred: Value, ) -> Result<(), VmExecutionError> { let asset_identifier = AssetIdentifier { contract_identifier: contract_identifier.clone(), asset_name: asset_name.clone(), }; self.get_asset_map()? - .add_asset_transfer(sender, asset_identifier, transfered); + .add_asset_transfer(sender, asset_identifier, transferred); Ok(()) } @@ -1698,30 +1749,30 @@ impl<'a, 'hooks> GlobalContext<'a, 'hooks> { sender: &PrincipalData, contract_identifier: &QualifiedContractIdentifier, asset_name: &ClarityName, - transfered: u128, + transferred: u128, ) -> Result<(), VmExecutionError> { let asset_identifier = AssetIdentifier { contract_identifier: contract_identifier.clone(), asset_name: asset_name.clone(), }; self.get_asset_map()? - .add_token_transfer(sender, asset_identifier, transfered) + .add_token_transfer(sender, asset_identifier, transferred) } pub fn log_stx_transfer( &mut self, sender: &PrincipalData, - transfered: u128, + transferred: u128, ) -> Result<(), VmExecutionError> { - self.get_asset_map()?.add_stx_transfer(sender, transfered) + self.get_asset_map()?.add_stx_transfer(sender, transferred) } pub fn log_stx_burn( &mut self, sender: &PrincipalData, - transfered: u128, + transferred: u128, ) -> Result<(), VmExecutionError> { - self.get_asset_map()?.add_stx_burn(sender, transfered) + self.get_asset_map()?.add_stx_burn(sender, transferred) } pub fn log_stacking( @@ -1758,7 +1809,7 @@ impl<'a, 'hooks> GlobalContext<'a, 'hooks> { ) -> std::result::Result where E: From, - F: FnOnce(&mut Environment) -> std::result::Result, + F: FnOnce(&mut ExecutionState, &InvocationContext) -> std::result::Result, { self.begin(); @@ -1766,15 +1817,17 @@ impl<'a, 'hooks> GlobalContext<'a, 'hooks> { // this right here is why it's dangerous to call this anywhere else. // the call stack gets reset to empyt each time! let mut callstack = CallStack::new(); - let mut exec_env = Environment::new( - self, - &contract_context, - &mut callstack, - Some(sender.clone()), - Some(sender), + let mut exec_state = ExecutionState { + global_context: self, + call_stack: &mut callstack, + }; + let invoke_ctx = InvocationContext { + contract_context: &contract_context, + sender: Some(sender.clone()), + caller: Some(sender), sponsor, - ); - f(&mut exec_env) + }; + f(&mut exec_state, &invoke_ctx) }; self.roll_back().map_err(ClarityEvalError::from)?; @@ -1927,6 +1980,7 @@ impl ContractContext { meta_nft: HashMap::new(), meta_ft: HashMap::new(), clarity_version, + is_deploying: false, } } @@ -1965,7 +2019,7 @@ impl ContractContext { /// Canonicalize the types for the specified epoch. Only functions and /// defined traits are exposed externally, so other types are not /// canonicalized. - pub fn canonicalize_types(&mut self, epoch: &StacksEpochId) { + pub fn canonicalize_types(&mut self, epoch: &StacksEpochId) -> Result<(), VmExecutionError> { for (_, function) in self.functions.iter_mut() { function.canonicalize_types(epoch); } @@ -1975,6 +2029,20 @@ impl ContractContext { *function = function.canonicalize(epoch); } } + + // In pre-sanitized-variable epochs, sanitize all contract + // variables at load time so lookups can borrow directly. + if epoch.uses_pre_sanitized_variables() { + for (_, value) in self.variables.iter_mut() { + let owned = std::mem::replace(value, Value::none()); + let (sanitized, _) = + Value::sanitize_value(epoch, &TypeSignature::type_of(&owned)?, owned) + .ok_or_else(|| RuntimeCheckErrorKind::CouldNotDetermineType)?; + *value = sanitized; + } + } + + Ok(()) } } @@ -2300,12 +2368,12 @@ mod test { epoch: StacksEpochId, mut tl_env_factory: TopLevelMemoryEnvironmentGenerator, ) { - let mut env = tl_env_factory.get_env(epoch); + let mut exec_state = tl_env_factory.get_env(epoch); let u1 = StacksAddress::new(0, Hash160([1; 20])).unwrap(); let u2 = StacksAddress::new(0, Hash160([2; 20])).unwrap(); // insufficient balance must be a non-includable transaction. it must error here, // not simply rollback the tx and squelch the error as includable. - let e = env + let e = exec_state .stx_transfer( &PrincipalData::from(u1), &PrincipalData::from(u2), @@ -2356,7 +2424,9 @@ mod test { .defined_traits .insert("bar".into(), trait_functions); - contract_context.canonicalize_types(&StacksEpochId::Epoch21); + contract_context + .canonicalize_types(&StacksEpochId::Epoch21) + .unwrap(); assert_eq!( contract_context.functions["foo"].get_arg_types()[0], @@ -2421,11 +2491,11 @@ mod test { fn eval_raw_empty_program() { // Setup environment let mut tl_env_factory = tl_env_factory(); - let mut env = tl_env_factory.get_env(StacksEpochId::latest()); + let mut exec_state = tl_env_factory.get_env(StacksEpochId::latest()); // Call eval_read_only with an empty program let program = ""; // empty program triggers parsed.is_empty() - let err = env.eval_raw(program).unwrap_err(); + let err = exec_state.eval_raw(program).unwrap_err(); let expected_err = ClarityEvalError::from(ParseError::new(ParseErrorKind::UnexpectedParserFailure)); assert_eq!(err, expected_err, "Expected a type parse failure"); @@ -2435,14 +2505,16 @@ mod test { fn eval_read_only_empty_program() { // Setup environment let mut tl_env_factory = tl_env_factory(); - let mut env = tl_env_factory.get_env(StacksEpochId::latest()); + let mut exec_state = tl_env_factory.get_env(StacksEpochId::latest()); // Construct a dummy contract context let contract_id = QualifiedContractIdentifier::local("dummy-contract").unwrap(); // Call eval_read_only with an empty program let program = ""; // empty program triggers parsed.is_empty() - let err = env.eval_read_only(&contract_id, program).unwrap_err(); + let err = exec_state + .eval_read_only(&contract_id, program) + .unwrap_err(); let expected_err = ClarityEvalError::from(ParseError::new(ParseErrorKind::UnexpectedParserFailure)); assert_eq!(err, expected_err, "Expected a type parse failure"); @@ -2490,28 +2562,44 @@ mod test { let contract_context = ContractContext::new(QualifiedContractIdentifier::transient(), version); - let mut env = Environment::new( - &mut global_context, - &contract_context, - &mut call_stack, - None, - None, - None, - ); + let mut exec_state = ExecutionState { + global_context: &mut global_context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { + contract_context: &contract_context, + sender: None, + caller: None, + sponsor: None, + }; let contract_id = QualifiedContractIdentifier::local("dup").unwrap(); let contract_src = "(define-public (ping) (ok u1))"; - let ast = ast::build_ast(&contract_id, contract_src, &mut env, version, epoch).unwrap(); + let ast = + ast::build_ast(&contract_id, contract_src, &mut exec_state, version, epoch).unwrap(); // First initialization succeeds - env.initialize_contract_from_ast(contract_id.clone(), version, &ast, contract_src) + exec_state + .initialize_contract_from_ast( + &invoke_ctx, + contract_id.clone(), + version, + &ast, + contract_src, + ) .unwrap(); // Second initialization hits ContractAlreadyExists - let err = env - .initialize_contract_from_ast(contract_id.clone(), version, &ast, contract_src) + let err = exec_state + .initialize_contract_from_ast( + &invoke_ctx, + contract_id.clone(), + version, + &ast, + contract_src, + ) .unwrap_err(); assert_eq!( diff --git a/clarity/src/vm/contracts.rs b/clarity/src/vm/contracts.rs index 23c0ecdd6d4..df4cee68dcb 100644 --- a/clarity/src/vm/contracts.rs +++ b/clarity/src/vm/contracts.rs @@ -39,6 +39,7 @@ impl Contract { version: ClarityVersion, ) -> Result { let mut contract_context = ContractContext::new(contract_identifier, version); + contract_context.is_deploying = true; eval_all( &contract.expressions, @@ -47,10 +48,11 @@ impl Contract { sponsor, )?; + contract_context.is_deploying = false; Ok(Contract { contract_context }) } - pub fn canonicalize_types(&mut self, epoch: &StacksEpochId) { - self.contract_context.canonicalize_types(epoch); + pub fn canonicalize_types(&mut self, epoch: &StacksEpochId) -> Result<(), VmExecutionError> { + self.contract_context.canonicalize_types(epoch) } } diff --git a/clarity/src/vm/costs/mod.rs b/clarity/src/vm/costs/mod.rs index cda9401eb20..e53ad007bcc 100644 --- a/clarity/src/vm/costs/mod.rs +++ b/clarity/src/vm/costs/mod.rs @@ -30,7 +30,7 @@ use stacks_common::types::StacksEpochId; use super::errors::{RuntimeCheckErrorKind, RuntimeError}; use crate::boot_util::boot_code_id; -use crate::vm::contexts::{ContractContext, GlobalContext}; +use crate::vm::contexts::{ContractContext, ExecutionState, GlobalContext, InvocationContext}; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::database::ClarityDatabase; use crate::vm::database::clarity_store::NullBackingStore; @@ -41,7 +41,7 @@ use crate::vm::types::signatures::TupleTypeSignature; use crate::vm::types::{ FunctionType, PrincipalData, QualifiedContractIdentifier, TupleData, TypeSignature, }; -use crate::vm::{CallStack, ClarityName, Environment, LocalContext, SymbolicExpression, Value}; +use crate::vm::{CallStack, ClarityName, LocalContext, SymbolicExpression, Value}; pub mod constants; pub mod cost_functions; #[allow(unused_variables)] @@ -91,9 +91,9 @@ pub fn runtime_cost, C: CostTracker>( } macro_rules! finally_drop_memory { - ( $env: expr, $used_mem:expr; $exec:expr ) => {{ + ( $gc: expr, $used_mem:expr; $exec:expr ) => {{ let result = (|| $exec)(); - $env.drop_memory($used_mem)?; + $gc.drop_memory($used_mem)?; result }}; } @@ -1050,10 +1050,10 @@ impl LimitedCostTracker { pub fn parse_cost( cost_function_name: &str, - eval_result: Result, VmExecutionError>, + eval_result: Result, ) -> Result { match eval_result { - Ok(Some(Value::Tuple(data))) => { + Ok(Value::Tuple(data)) => { let results = ( data.data_map.get("write_length"), data.data_map.get("write_count"), @@ -1081,12 +1081,9 @@ pub fn parse_cost( )), } } - Ok(Some(_)) => Err(CostErrors::CostComputationFailed( + Ok(_) => Err(CostErrors::CostComputationFailed( "Clarity cost function returned something other than a Cost tuple".to_string(), )), - Ok(None) => Err(CostErrors::CostComputationFailed( - "Clarity cost function returned nothing".to_string(), - )), Err(e) => Err(CostErrors::CostComputationFailed(format!( "Error evaluating result of cost function {cost_function_name}: {e}" ))), @@ -1135,19 +1132,19 @@ pub fn compute_cost( let context = LocalContext::new(); let mut call_stack = CallStack::new(); let publisher: PrincipalData = cost_contract.contract_identifier.issuer.clone().into(); - let mut env = Environment::new( + let mut env = ExecutionState { global_context, - cost_contract, - &mut call_stack, - Some(publisher.clone()), - Some(publisher.clone()), - None, - ); - - let result = super::eval(&function_invocation, &mut env, &context)?; - Ok(Some(result)) + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { + contract_context: cost_contract, + sender: Some(publisher.clone()), + caller: Some(publisher.clone()), + sponsor: None, + }; + super::eval(&function_invocation, &mut env, &invoke_ctx, &context) + .and_then(|v| v.clone_with_cost(&mut env)) }); - parse_cost(&cost_function_reference.to_string(), eval_result) } diff --git a/clarity/src/vm/database/clarity_db.rs b/clarity/src/vm/database/clarity_db.rs index 6d0adc1f480..30728694fad 100644 --- a/clarity/src/vm/database/clarity_db.rs +++ b/clarity/src/vm/database/clarity_db.rs @@ -850,7 +850,7 @@ impl<'a> ClarityDatabase<'a> { .ok_or_else(|| VmInternalError::Expect( "Failed to read non-consensus contract metadata, even though contract exists in MARF." .into()))?; - data.canonicalize_types(&self.get_clarity_epoch_version()?); + data.canonicalize_types(&self.get_clarity_epoch_version()?)?; Ok(data) } @@ -1576,7 +1576,7 @@ impl ClarityDatabase<'_> { { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(variable_descriptor.value_type.clone()), - Box::new(value), + value.to_error_string(), ) .into()); } @@ -1737,7 +1737,7 @@ impl ClarityDatabase<'_> { { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(map_descriptor.key_type.clone()), - Box::new(key_value.clone()), + key_value.to_error_string(), ) .into()); } @@ -1768,7 +1768,7 @@ impl ClarityDatabase<'_> { { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(map_descriptor.key_type.clone()), - Box::new(key_value.clone()), + key_value.to_error_string(), ) .into()); } @@ -1913,7 +1913,7 @@ impl ClarityDatabase<'_> { { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(map_descriptor.key_type.clone()), - Box::new(key_value), + key_value.to_error_string(), ) .into()); } @@ -1923,7 +1923,7 @@ impl ClarityDatabase<'_> { { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(map_descriptor.value_type.clone()), - Box::new(value), + value.to_error_string(), ) .into()); } @@ -1974,7 +1974,7 @@ impl ClarityDatabase<'_> { { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(map_descriptor.key_type.clone()), - Box::new(key_value.clone()), + key_value.to_error_string(), ) .into()); } @@ -2198,7 +2198,7 @@ impl ClarityDatabase<'_> { if !key_type.admits(&self.get_clarity_epoch_version()?, asset)? { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(key_type.clone()), - Box::new(asset.clone()), + asset.to_error_string(), ) .into()); } @@ -2258,7 +2258,7 @@ impl ClarityDatabase<'_> { if !key_type.admits(&self.get_clarity_epoch_version()?, asset)? { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(key_type.clone()), - Box::new(asset.clone()), + asset.to_error_string(), ) .into()); } @@ -2289,7 +2289,7 @@ impl ClarityDatabase<'_> { if !key_type.admits(&self.get_clarity_epoch_version()?, asset)? { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(key_type.clone()), - Box::new(asset.clone()), + asset.to_error_string(), ) .into()); } diff --git a/clarity/src/vm/docs/mod.rs b/clarity/src/vm/docs/mod.rs index c9a0906f87c..fcbe2fca539 100644 --- a/clarity/src/vm/docs/mod.rs +++ b/clarity/src/vm/docs/mod.rs @@ -1410,11 +1410,12 @@ but in Clarity 5 and later, the `message-hash` is used directly without addition High-S signatures are allowed. Note that this is NOT the Bitcoin (or default Stacks) signature scheme, secp256k1, but rather the NIST P-256 curve (also known as secp256r1).", - example: "(secp256r1-verify 0xc3abef6a775793dfbc8e0719e7a1de1fc2f90d37a7912b1ce8e300a5a03b06a8 - 0xf2b8c0645caa7250e3b96d633cf40a88456e4ffbddffb69200c4e019039dfd310eac59293c23e6d6aa8b0c5d9e4e48fa4c4fdf1ace2ba618dc0263b5e90a0903 0x031e18532fd4754c02f3041d9c75ceb33b83ffd81ac7ce4fe882ccb1c98bc5896e) ;; Returns true + example: "(secp256r1-verify 0x44acf6b7e36c1342c2c5897204fe09504e1e2efb1a900377dbc4e7a6a133ec56 + 0xf3ac8061b514795b8843e3d6629527ed2afd6b1f6a555a7acabb5e6f79c8c2ac8bf77819ca05a6b2786c76262bf7371cef97b218e96f175a3ccdda2acc058903 + 0x031ccbe91c075fc7f4f033bfa248db8fccd3565de94bbfb12f3c59ff46c271bf83) ;; Returns true (secp256r1-verify 0x0000000000000000000000000000000000000000000000000000000000000000 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 - 0x037a6b62e3c8b14f1b5933f5d5ab0509a8e7d95a111b8d3b264d95bfa753b00296) ;; Returns false" + 0x031ccbe91c075fc7f4f033bfa248db8fccd3565de94bbfb12f3c59ff46c271bf83) ;; Returns false" }; const CONTRACT_CALL_API: SpecialAPI = SpecialAPI { @@ -1470,7 +1471,9 @@ const AT_BLOCK: SpecialAPI = SpecialAPI { snippet: "at-block ${1:id-header-hash} ${2:expr}", output_type: "A", signature: "(at-block id-block-hash expr)", - description: "The `at-block` function evaluates the expression `expr` _as if_ it were evaluated at the end of the + description: "Removed in Epoch 3.4 (see SIP-042). + +The `at-block` function evaluates the expression `expr` _as if_ it were evaluated at the end of the block indicated by the _block-hash_ argument. The `expr` closure must be read-only. Note: The block identifying hash must be a hash returned by the `id-header-hash` block information @@ -3474,7 +3477,7 @@ mod test { QualifiedContractIdentifier::local("tokens").unwrap().into(), None, None, - |e| { + |e, _invoke_ctx| { let mut snapshot = e .global_context .database diff --git a/clarity/src/vm/functions/arithmetic.rs b/clarity/src/vm/functions/arithmetic.rs index 748e0ee5c77..444cb5eb50b 100644 --- a/clarity/src/vm/functions/arithmetic.rs +++ b/clarity/src/vm/functions/arithmetic.rs @@ -18,6 +18,7 @@ use std::cmp; use integer_sqrt::IntegerSquareRoot; +use crate::vm::contexts::{ExecutionState, InvocationContext}; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::runtime_cost; use crate::vm::errors::{ @@ -28,7 +29,7 @@ use crate::vm::types::{ ASCIIData, BuffData, CharType, SequenceData, TypeSignature, UTF8Data, Value, }; use crate::vm::version::ClarityVersion; -use crate::vm::{Environment, LocalContext, eval}; +use crate::vm::{LocalContext, eval}; struct U128Ops(); struct I128Ops(); @@ -78,7 +79,7 @@ macro_rules! type_force_binary_arithmetic { (Value::UInt(x), Value::UInt(y)) => U128Ops::$function(x, y), (x, _) => Err(RuntimeCheckErrorKind::UnionTypeValueError( vec![TypeSignature::IntType, TypeSignature::UIntType], - Box::new(x), + x.to_error_string(), ) .into()), } @@ -87,13 +88,13 @@ macro_rules! type_force_binary_arithmetic { // The originally supported comparable types in Clarity1 were Int and UInt. macro_rules! type_force_binary_comparison_v1 { - ($function: ident, $x: expr, $y: expr) => {{ - match ($x, $y) { + ($function: ident, $x: expr, $y: expr, $e: expr) => {{ + match ($x.as_ref(), $y.as_ref()) { (Value::Int(x), Value::Int(y)) => I128Ops::$function(x, y), (Value::UInt(x), Value::UInt(y)) => U128Ops::$function(x, y), - (x, _) => Err(RuntimeCheckErrorKind::UnionTypeValueError( + (_, _) => Err(RuntimeCheckErrorKind::UnionTypeValueError( vec![TypeSignature::IntType, TypeSignature::UIntType], - Box::new(x), + $x.as_ref().to_error_string(), ) .into()), } @@ -103,8 +104,8 @@ macro_rules! type_force_binary_comparison_v1 { // Clarity2 adds supported comparable types ASCII, UTF8 and Buffer. These are only // accessed if the ClarityVersion, as read by the SpecialFunction, is >= 2. macro_rules! type_force_binary_comparison_v2 { - ($function: ident, $x: expr, $y: expr) => {{ - match ($x, $y) { + ($function: ident, $x: expr, $y: expr, $e: expr) => {{ + match ($x.as_ref(), $y.as_ref()) { (Value::Int(x), Value::Int(y)) => I128Ops::$function(x, y), (Value::UInt(x), Value::UInt(y)) => U128Ops::$function(x, y), ( @@ -119,7 +120,7 @@ macro_rules! type_force_binary_comparison_v2 { Value::Sequence(SequenceData::Buffer(BuffData { data: x })), Value::Sequence(SequenceData::Buffer(BuffData { data: y })), ) => BuffOps::$function(x, y), - (x, _) => Err(RuntimeCheckErrorKind::UnionTypeValueError( + (_, _) => Err(RuntimeCheckErrorKind::UnionTypeValueError( vec![ TypeSignature::IntType, TypeSignature::UIntType, @@ -127,7 +128,7 @@ macro_rules! type_force_binary_comparison_v2 { TypeSignature::STRING_UTF8_MAX, TypeSignature::BUFFER_MAX, ], - Box::new(x), + $x.as_ref().to_error_string(), ) .into()), } @@ -141,7 +142,7 @@ macro_rules! type_force_unary_arithmetic { Value::UInt(x) => U128Ops::$function(x), x => Err(RuntimeCheckErrorKind::UnionTypeValueError( vec![TypeSignature::IntType, TypeSignature::UIntType], - Box::new(x), + x.to_error_string(), ) .into()), } @@ -167,7 +168,7 @@ macro_rules! type_force_variadic_arithmetic { Value::Int(value) => Ok(value), _ => Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::IntType), - Box::new(x.clone()), + x.to_error_string(), )), }) .collect(); @@ -181,7 +182,7 @@ macro_rules! type_force_variadic_arithmetic { Value::UInt(value) => Ok(value), _ => Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::UIntType), - Box::new(x.clone()), + x.to_error_string(), )), }) .collect(); @@ -190,7 +191,7 @@ macro_rules! type_force_variadic_arithmetic { } _ => Err(RuntimeCheckErrorKind::UnionTypeValueError( vec![TypeSignature::IntType, TypeSignature::UIntType], - Box::new(first.clone()), + first.to_error_string(), ) .into()), } @@ -202,16 +203,16 @@ macro_rules! type_force_variadic_arithmetic { macro_rules! make_comparison_ops { ($struct_name: ident, $type:ty) => { impl $struct_name { - fn greater(x: $type, y: $type) -> Result { + fn greater(x: &$type, y: &$type) -> Result { Ok(Value::Bool(x > y)) } - fn less(x: $type, y: $type) -> Result { + fn less(x: &$type, y: &$type) -> Result { Ok(Value::Bool(x < y)) } - fn leq(x: $type, y: $type) -> Result { + fn leq(x: &$type, y: &$type) -> Result { Ok(Value::Bool(x <= y)) } - fn geq(x: $type, y: $type) -> Result { + fn geq(x: &$type, y: &$type) -> Result { Ok(Value::Bool(x >= y)) } } @@ -388,45 +389,48 @@ pub fn native_bitwise_not(a: Value) -> Result { // the clarity version. fn special_geq_v1( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; - let a = eval(&args[0], env, context)?; - let b = eval(&args[1], env, context)?; - runtime_cost(ClarityCostFunction::Geq, env, args.len())?; - type_force_binary_comparison_v1!(geq, a, b) + let a = eval(&args[0], exec_state, invoke_ctx, context)?; + let b = eval(&args[1], exec_state, invoke_ctx, context)?; + runtime_cost(ClarityCostFunction::Geq, exec_state, args.len())?; + type_force_binary_comparison_v1!(geq, a, b, exec_state) } // This function is 'special', because it must access the context to determine // the clarity version. fn special_geq_v2( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; - let a = eval(&args[0], env, context)?; - let b = eval(&args[1], env, context)?; + let a = eval(&args[0], exec_state, invoke_ctx, context)?; + let b = eval(&args[1], exec_state, invoke_ctx, context)?; runtime_cost( ClarityCostFunction::Geq, - env, - cmp::min(a.size()?, b.size()?), + exec_state, + cmp::min(a.as_ref().size()?, b.as_ref().size()?), )?; - type_force_binary_comparison_v2!(geq, a, b) + type_force_binary_comparison_v2!(geq, a, b, exec_state) } // This function is 'special', because it must access the context to determine // the clarity version. pub fn special_geq( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { - if *env.contract_context.get_clarity_version() >= ClarityVersion::Clarity2 { - special_geq_v2(args, env, context) + if *invoke_ctx.contract_context.get_clarity_version() >= ClarityVersion::Clarity2 { + special_geq_v2(args, exec_state, invoke_ctx, context) } else { - special_geq_v1(args, env, context) + special_geq_v1(args, exec_state, invoke_ctx, context) } } @@ -435,45 +439,48 @@ pub fn special_geq( // 2.05 and earlier fn special_leq_v1( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; - let a = eval(&args[0], env, context)?; - let b = eval(&args[1], env, context)?; - runtime_cost(ClarityCostFunction::Leq, env, args.len())?; - type_force_binary_comparison_v1!(leq, a, b) + let a = eval(&args[0], exec_state, invoke_ctx, context)?; + let b = eval(&args[1], exec_state, invoke_ctx, context)?; + runtime_cost(ClarityCostFunction::Leq, exec_state, args.len())?; + type_force_binary_comparison_v1!(leq, a, b, exec_state) } // This function is 'special', because it must access the context to determine // the clarity version. fn special_leq_v2( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; - let a = eval(&args[0], env, context)?; - let b = eval(&args[1], env, context)?; + let a = eval(&args[0], exec_state, invoke_ctx, context)?; + let b = eval(&args[1], exec_state, invoke_ctx, context)?; runtime_cost( ClarityCostFunction::Leq, - env, - cmp::min(a.size()?, b.size()?), + exec_state, + cmp::min(a.as_ref().size()?, b.as_ref().size()?), )?; - type_force_binary_comparison_v2!(leq, a, b) + type_force_binary_comparison_v2!(leq, a, b, exec_state) } // This function is 'special', because it must access the context to determine // the clarity version. pub fn special_leq( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { - if *env.contract_context.get_clarity_version() >= ClarityVersion::Clarity2 { - special_leq_v2(args, env, context) + if *invoke_ctx.contract_context.get_clarity_version() >= ClarityVersion::Clarity2 { + special_leq_v2(args, exec_state, invoke_ctx, context) } else { - special_leq_v1(args, env, context) + special_leq_v1(args, exec_state, invoke_ctx, context) } } @@ -481,41 +488,48 @@ pub fn special_leq( // the clarity version. fn special_greater_v1( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; - let a = eval(&args[0], env, context)?; - let b = eval(&args[1], env, context)?; - runtime_cost(ClarityCostFunction::Ge, env, args.len())?; - type_force_binary_comparison_v1!(greater, a, b) + let a = eval(&args[0], exec_state, invoke_ctx, context)?; + let b = eval(&args[1], exec_state, invoke_ctx, context)?; + runtime_cost(ClarityCostFunction::Ge, exec_state, args.len())?; + type_force_binary_comparison_v1!(greater, a, b, exec_state) } // This function is 'special', because it must access the context to determine // the clarity version. fn special_greater_v2( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; - let a = eval(&args[0], env, context)?; - let b = eval(&args[1], env, context)?; - runtime_cost(ClarityCostFunction::Ge, env, cmp::min(a.size()?, b.size()?))?; - type_force_binary_comparison_v2!(greater, a, b) + let a = eval(&args[0], exec_state, invoke_ctx, context)?; + let b = eval(&args[1], exec_state, invoke_ctx, context)?; + runtime_cost( + ClarityCostFunction::Ge, + exec_state, + cmp::min(a.as_ref().size()?, b.as_ref().size()?), + )?; + type_force_binary_comparison_v2!(greater, a, b, exec_state) } // This function is 'special', because it must access the context to determine // the clarity version. pub fn special_greater( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { - if *env.contract_context.get_clarity_version() >= ClarityVersion::Clarity2 { - special_greater_v2(args, env, context) + if *invoke_ctx.contract_context.get_clarity_version() >= ClarityVersion::Clarity2 { + special_greater_v2(args, exec_state, invoke_ctx, context) } else { - special_greater_v1(args, env, context) + special_greater_v1(args, exec_state, invoke_ctx, context) } } @@ -523,41 +537,48 @@ pub fn special_greater( // the clarity version. fn special_less_v1( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; - let a = eval(&args[0], env, context)?; - let b = eval(&args[1], env, context)?; - runtime_cost(ClarityCostFunction::Le, env, args.len())?; - type_force_binary_comparison_v1!(less, a, b) + let a = eval(&args[0], exec_state, invoke_ctx, context)?; + let b = eval(&args[1], exec_state, invoke_ctx, context)?; + runtime_cost(ClarityCostFunction::Le, exec_state, args.len())?; + type_force_binary_comparison_v1!(less, a, b, exec_state) } // This function is 'special', because it must access the context to determine // the clarity version. fn special_less_v2( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; - let a = eval(&args[0], env, context)?; - let b = eval(&args[1], env, context)?; - runtime_cost(ClarityCostFunction::Le, env, cmp::min(a.size()?, b.size()?))?; - type_force_binary_comparison_v2!(less, a, b) + let a = eval(&args[0], exec_state, invoke_ctx, context)?; + let b = eval(&args[1], exec_state, invoke_ctx, context)?; + runtime_cost( + ClarityCostFunction::Le, + exec_state, + cmp::min(a.as_ref().size()?, b.as_ref().size()?), + )?; + type_force_binary_comparison_v2!(less, a, b, exec_state) } // This function is 'special', because it must access the context to determine // the clarity version. pub fn special_less( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { - if *env.contract_context.get_clarity_version() >= ClarityVersion::Clarity2 { - special_less_v2(args, env, context) + if *invoke_ctx.contract_context.get_clarity_version() >= ClarityVersion::Clarity2 { + special_less_v2(args, exec_state, invoke_ctx, context) } else { - special_less_v1(args, env, context) + special_less_v1(args, exec_state, invoke_ctx, context) } } @@ -608,10 +629,11 @@ pub fn native_bitwise_left_shift(input: Value, pos: Value) -> Result Result Result { } else { Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::IntType), - Box::new(input), + input.to_error_string(), ) .into()) } @@ -664,7 +687,7 @@ pub fn native_to_int(input: Value) -> Result { } else { Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::UIntType), - Box::new(input), + input.to_error_string(), ) .into()) } diff --git a/clarity/src/vm/functions/assets.rs b/clarity/src/vm/functions/assets.rs index 70847a0a731..4a62c0538b5 100644 --- a/clarity/src/vm/functions/assets.rs +++ b/clarity/src/vm/functions/assets.rs @@ -16,6 +16,7 @@ use stacks_common::types::StacksEpochId; +use crate::vm::contexts::{ExecutionState, InvocationContext}; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::{CostTracker, runtime_cost}; use crate::vm::database::STXBalance; @@ -26,7 +27,7 @@ use crate::vm::representations::SymbolicExpression; use crate::vm::types::{ AssetIdentifier, BuffData, PrincipalData, SequenceData, TupleData, TypeSignature, Value, }; -use crate::vm::{Environment, LocalContext, eval}; +use crate::vm::{LocalContext, eval}; enum MintAssetErrorCodes { ALREADY_EXIST = 1, @@ -88,18 +89,19 @@ switch_on_global_epoch!(special_burn_asset( pub fn special_stx_balance( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(1, args)?; - runtime_cost(ClarityCostFunction::StxBalance, env, 0)?; + runtime_cost(ClarityCostFunction::StxBalance, exec_state, 0)?; - let owner = eval(&args[0], env, context)?; + let owner = eval(&args[0], exec_state, invoke_ctx, context)?; - if let Value::Principal(ref principal) = owner { + if let Value::Principal(principal) = owner.as_ref() { let balance = { - let mut snapshot = env + let mut snapshot = exec_state .global_context .database .get_stx_balance_snapshot(principal)?; @@ -109,7 +111,7 @@ pub fn special_stx_balance( } else { Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::PrincipalType), - Box::new(owner), + owner.as_ref().to_error_string(), ) .into()) } @@ -119,7 +121,8 @@ pub fn special_stx_balance( /// If the 'from' principal has locked STX, and they have unlocked, then process the STX unlock /// and update its balance in addition to spending tokens out of it. pub fn stx_transfer_consolidated( - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, from: &PrincipalData, to: &PrincipalData, amount: u128, @@ -133,53 +136,61 @@ pub fn stx_transfer_consolidated( return clarity_ecode!(StxErrorCodes::SENDER_IS_RECIPIENT); } - if Some(from) != env.sender.as_ref() { + if Some(from) != invoke_ctx.sender.as_ref() { return clarity_ecode!(StxErrorCodes::SENDER_IS_NOT_TX_SENDER); } // loading from/to principals and balances - env.add_memory(TypeSignature::PrincipalType.size()?.into())?; - env.add_memory(TypeSignature::PrincipalType.size()?.into())?; + exec_state.add_memory(TypeSignature::PrincipalType.size()?.into())?; + exec_state.add_memory(TypeSignature::PrincipalType.size()?.into())?; // loading from's locked amount and height // TODO: this does not count the inner stacks block header load, but arguably, // this could be optimized away, so it shouldn't penalize the caller. - env.add_memory(STXBalance::unlocked_and_v1_size as u64)?; - env.add_memory(STXBalance::unlocked_and_v1_size as u64)?; + exec_state.add_memory(STXBalance::unlocked_and_v1_size as u64)?; + exec_state.add_memory(STXBalance::unlocked_and_v1_size as u64)?; - let mut sender_snapshot = env.global_context.database.get_stx_balance_snapshot(from)?; + let mut sender_snapshot = exec_state + .global_context + .database + .get_stx_balance_snapshot(from)?; if !sender_snapshot.can_transfer(amount)? { return clarity_ecode!(StxErrorCodes::NOT_ENOUGH_BALANCE); } sender_snapshot.transfer_to(to, amount)?; - env.global_context.log_stx_transfer(from, amount)?; - env.register_stx_transfer_event(from.clone(), to.clone(), amount, memo.clone())?; + exec_state.global_context.log_stx_transfer(from, amount)?; + exec_state.register_stx_transfer_event(from.clone(), to.clone(), amount, memo.clone())?; Ok(Value::okay_true()) } pub fn special_stx_transfer( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(3, args)?; - runtime_cost(ClarityCostFunction::StxTransfer, env, 0)?; + runtime_cost(ClarityCostFunction::StxTransfer, exec_state, 0)?; - let amount_val = eval(&args[0], env, context)?; - let from_val = eval(&args[1], env, context)?; - let to_val = eval(&args[2], env, context)?; + let amount_val = eval(&args[0], exec_state, invoke_ctx, context)?; + let from_val = eval(&args[1], exec_state, invoke_ctx, context)?; + let to_val = eval(&args[2], exec_state, invoke_ctx, context)?; let memo_val = Value::Sequence(SequenceData::Buffer(BuffData::empty())); if let ( - Value::Principal(ref from), - Value::Principal(ref to), + Value::Principal(from), + Value::Principal(to), Value::UInt(amount), - Value::Sequence(SequenceData::Buffer(ref memo)), - ) = (from_val, to_val, amount_val, memo_val) - { - stx_transfer_consolidated(env, from, to, amount, memo) + Value::Sequence(SequenceData::Buffer(memo)), + ) = ( + from_val.as_ref(), + to_val.as_ref(), + amount_val.as_ref(), + &memo_val, + ) { + stx_transfer_consolidated(exec_state, invoke_ctx, from, to, *amount, memo) } else { Err(RuntimeCheckErrorKind::Unreachable("Bad transfer STX args".to_string()).into()) } @@ -187,25 +198,30 @@ pub fn special_stx_transfer( pub fn special_stx_transfer_memo( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(4, args)?; - runtime_cost(ClarityCostFunction::StxTransferMemo, env, 0)?; + runtime_cost(ClarityCostFunction::StxTransferMemo, exec_state, 0)?; - let amount_val = eval(&args[0], env, context)?; - let from_val = eval(&args[1], env, context)?; - let to_val = eval(&args[2], env, context)?; - let memo_val = eval(&args[3], env, context)?; + let amount_val = eval(&args[0], exec_state, invoke_ctx, context)?; + let from_val = eval(&args[1], exec_state, invoke_ctx, context)?; + let to_val = eval(&args[2], exec_state, invoke_ctx, context)?; + let memo_val = eval(&args[3], exec_state, invoke_ctx, context)?; if let ( - Value::Principal(ref from), - Value::Principal(ref to), + Value::Principal(from), + Value::Principal(to), Value::UInt(amount), - Value::Sequence(SequenceData::Buffer(ref memo)), - ) = (from_val, to_val, amount_val, memo_val) - { - stx_transfer_consolidated(env, from, to, amount, memo) + Value::Sequence(SequenceData::Buffer(memo)), + ) = ( + from_val.as_ref(), + to_val.as_ref(), + amount_val.as_ref(), + memo_val.as_ref(), + ) { + stx_transfer_consolidated(exec_state, invoke_ctx, from, to, *amount, memo) } else { Err(RuntimeCheckErrorKind::Unreachable("Bad transfer STX args".to_string()).into()) } @@ -214,32 +230,33 @@ pub fn special_stx_transfer_memo( #[allow(clippy::unnecessary_fallible_conversions)] pub fn special_stx_account( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(1, args)?; - runtime_cost(ClarityCostFunction::StxGetAccount, env, 0)?; + runtime_cost(ClarityCostFunction::StxGetAccount, exec_state, 0)?; - let owner = eval(&args[0], env, context)?; - let principal = if let Value::Principal(p) = owner { + let owner = eval(&args[0], exec_state, invoke_ctx, context)?; + let principal = if let Value::Principal(p) = owner.as_ref() { p } else { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::PrincipalType), - Box::new(owner), + owner.as_ref().to_error_string(), ) .into()); }; - let stx_balance = env + let stx_balance = exec_state .global_context .database - .get_stx_balance_snapshot(&principal)? + .get_stx_balance_snapshot(principal)? .canonical_balance_repr()?; - let v1_unlock_ht = env.global_context.database.get_v1_unlock_height(); - let v2_unlock_ht = env.global_context.database.get_v2_unlock_height()?; - let v3_unlock_ht = env.global_context.database.get_v3_unlock_height()?; + let v1_unlock_ht = exec_state.global_context.database.get_v1_unlock_height(); + let v2_unlock_ht = exec_state.global_context.database.get_v2_unlock_height()?; + let v3_unlock_ht = exec_state.global_context.database.get_v3_unlock_height()?; Ok(TupleData::from_data(vec![ ( @@ -270,46 +287,52 @@ pub fn special_stx_account( pub fn special_stx_burn( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; - runtime_cost(ClarityCostFunction::StxTransfer, env, 0)?; + runtime_cost(ClarityCostFunction::StxTransfer, exec_state, 0)?; - let amount_val = eval(&args[0], env, context)?; - let from_val = eval(&args[1], env, context)?; + let amount_val = eval(&args[0], exec_state, invoke_ctx, context)?; + let from_val = eval(&args[1], exec_state, invoke_ctx, context)?; - if let (Value::Principal(from), Value::UInt(amount)) = (&from_val, amount_val) { - if amount == 0 { + if let (Value::Principal(from), Value::UInt(amount)) = (from_val.as_ref(), amount_val.as_ref()) + { + if *amount == 0 { return clarity_ecode!(StxErrorCodes::NON_POSITIVE_AMOUNT); } - if Some(from) != env.sender.as_ref() { + if Some(from) != invoke_ctx.sender.as_ref() { return clarity_ecode!(StxErrorCodes::SENDER_IS_NOT_TX_SENDER); } - env.add_memory(TypeSignature::PrincipalType.size()?.into())?; - env.add_memory(STXBalance::unlocked_and_v1_size.try_into().map_err(|_| { + exec_state.add_memory(TypeSignature::PrincipalType.size()?.into())?; + exec_state.add_memory(STXBalance::unlocked_and_v1_size.try_into().map_err(|_| { RuntimeCheckErrorKind::Unreachable( "BUG: STXBalance::unlocked_and_v1_size does not fit into a u64".into(), ) })?)?; - let mut burner_snapshot = env.global_context.database.get_stx_balance_snapshot(from)?; - if !burner_snapshot.can_transfer(amount)? { + let mut burner_snapshot = exec_state + .global_context + .database + .get_stx_balance_snapshot(from)?; + if !burner_snapshot.can_transfer(*amount)? { return clarity_ecode!(StxErrorCodes::NOT_ENOUGH_BALANCE); } - burner_snapshot.debit(amount)?; + burner_snapshot.debit(*amount)?; burner_snapshot.save()?; - env.global_context + exec_state + .global_context .database - .decrement_ustx_liquid_supply(amount)?; + .decrement_ustx_liquid_supply(*amount)?; - env.global_context.log_stx_burn(from, amount)?; - env.register_stx_burn_event(from.clone(), amount)?; + exec_state.global_context.log_stx_burn(from, *amount)?; + exec_state.register_stx_burn_event(from.clone(), *amount)?; Ok(Value::okay_true()) } else { @@ -319,12 +342,13 @@ pub fn special_stx_burn( pub fn special_mint_token( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(3, args)?; - runtime_cost(ClarityCostFunction::FtMint, env, 0)?; + runtime_cost(ClarityCostFunction::FtMint, exec_state, 0)?; let token_name = args[0] .match_atom() @@ -332,51 +356,54 @@ pub fn special_mint_token( "Bad token name".to_string(), ))?; - let amount = eval(&args[1], env, context)?; - let to = eval(&args[2], env, context)?; + let amount = eval(&args[1], exec_state, invoke_ctx, context)?; + let to = eval(&args[2], exec_state, invoke_ctx, context)?; - if let (Value::UInt(amount), Value::Principal(ref to_principal)) = (amount, to) { - if amount == 0 { + if let (Value::UInt(amount), Value::Principal(to_principal)) = (amount.as_ref(), to.as_ref()) { + if *amount == 0 { return clarity_ecode!(MintTokenErrorCodes::NON_POSITIVE_AMOUNT); } - let ft_info = env.contract_context.meta_ft.get(token_name).ok_or( + let ft_info = invoke_ctx.contract_context.meta_ft.get(token_name).ok_or( RuntimeCheckErrorKind::Unreachable(format!("No such FT: {token_name}")), )?; - env.global_context.database.checked_increase_token_supply( - &env.contract_context.contract_identifier, - token_name, - amount, - ft_info, - )?; - - let to_bal = env.global_context.database.get_ft_balance( - &env.contract_context.contract_identifier, + exec_state + .global_context + .database + .checked_increase_token_supply( + &invoke_ctx.contract_context.contract_identifier, + token_name, + *amount, + ft_info, + )?; + + let to_bal = exec_state.global_context.database.get_ft_balance( + &invoke_ctx.contract_context.contract_identifier, token_name, to_principal, Some(ft_info), )?; let final_to_bal = to_bal - .checked_add(amount) + .checked_add(*amount) .ok_or_else(|| VmInternalError::Expect("STX overflow".into()))?; - env.add_memory(TypeSignature::PrincipalType.size()?.into())?; - env.add_memory(TypeSignature::UIntType.size()?.into())?; + exec_state.add_memory(TypeSignature::PrincipalType.size()?.into())?; + exec_state.add_memory(TypeSignature::UIntType.size()?.into())?; - env.global_context.database.set_ft_balance( - &env.contract_context.contract_identifier, + exec_state.global_context.database.set_ft_balance( + &invoke_ctx.contract_context.contract_identifier, token_name, to_principal, final_to_bal, )?; let asset_identifier = AssetIdentifier { - contract_identifier: env.contract_context.contract_identifier.clone(), + contract_identifier: invoke_ctx.contract_context.contract_identifier.clone(), asset_name: token_name.clone(), }; - env.register_ft_mint_event(to_principal.clone(), amount, asset_identifier)?; + exec_state.register_ft_mint_event(to_principal.clone(), *amount, asset_identifier)?; Ok(Value::okay_true()) } else { @@ -386,7 +413,8 @@ pub fn special_mint_token( pub fn special_mint_asset_v200( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(3, args)?; @@ -397,37 +425,33 @@ pub fn special_mint_asset_v200( "Bad token name".to_string(), ))?; - let asset = eval(&args[1], env, context)?; - let to = eval(&args[2], env, context)?; + let asset = eval(&args[1], exec_state, invoke_ctx, context)?; + let to = eval(&args[2], exec_state, invoke_ctx, context)?; - let nft_metadata = - env.contract_context - .meta_nft - .get(asset_name) - .ok_or(RuntimeCheckErrorKind::Unreachable(format!( - "No such NFT: {asset_name}" - )))?; + let nft_metadata = invoke_ctx.contract_context.meta_nft.get(asset_name).ok_or( + RuntimeCheckErrorKind::Unreachable(format!("No such NFT: {asset_name}")), + )?; let expected_asset_type = &nft_metadata.key_type; runtime_cost( ClarityCostFunction::NftMint, - env, + exec_state, expected_asset_type.size()?, )?; - if !expected_asset_type.admits(env.epoch(), &asset)? { + if !expected_asset_type.admits(exec_state.epoch(), asset.as_ref())? { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(expected_asset_type.clone()), - Box::new(asset), + asset.as_ref().to_error_string(), ) .into()); } - if let Value::Principal(ref to_principal) = to { - match env.global_context.database.get_nft_owner( - &env.contract_context.contract_identifier, + if let Value::Principal(to_principal) = to.as_ref() { + match exec_state.global_context.database.get_nft_owner( + &invoke_ctx.contract_context.contract_identifier, asset_name, - &asset, + asset.as_ref(), expected_asset_type, ) { Err(VmExecutionError::Runtime(RuntimeError::NoSuchToken, _)) => Ok(()), @@ -435,30 +459,31 @@ pub fn special_mint_asset_v200( Err(e) => Err(e), }?; - env.add_memory(TypeSignature::PrincipalType.size()?.into())?; - env.add_memory(expected_asset_type.size()?.into())?; + exec_state.add_memory(TypeSignature::PrincipalType.size()?.into())?; + exec_state.add_memory(expected_asset_type.size()?.into())?; - let epoch = *env.epoch(); - env.global_context.database.set_nft_owner( - &env.contract_context.contract_identifier, + let epoch = *exec_state.epoch(); + exec_state.global_context.database.set_nft_owner( + &invoke_ctx.contract_context.contract_identifier, asset_name, - &asset, + asset.as_ref(), to_principal, expected_asset_type, &epoch, )?; let asset_identifier = AssetIdentifier { - contract_identifier: env.contract_context.contract_identifier.clone(), + contract_identifier: invoke_ctx.contract_context.contract_identifier.clone(), asset_name: asset_name.clone(), }; - env.register_nft_mint_event(to_principal.clone(), asset, asset_identifier)?; + let asset = asset.clone_with_cost(exec_state)?; + exec_state.register_nft_mint_event(to_principal.clone(), asset, asset_identifier)?; Ok(Value::okay_true()) } else { Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::PrincipalType), - Box::new(to), + to.as_ref().to_error_string(), ) .into()) } @@ -468,7 +493,8 @@ pub fn special_mint_asset_v200( /// asset as input to the cost tabulation. Otherwise identical to v200. pub fn special_mint_asset_v205( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(3, args)?; @@ -479,36 +505,33 @@ pub fn special_mint_asset_v205( "Bad token name".to_string(), ))?; - let asset = eval(&args[1], env, context)?; - let to = eval(&args[2], env, context)?; + let asset = eval(&args[1], exec_state, invoke_ctx, context)?; + let to = eval(&args[2], exec_state, invoke_ctx, context)?; - let nft_metadata = - env.contract_context - .meta_nft - .get(asset_name) - .ok_or(RuntimeCheckErrorKind::Unreachable(format!( - "No such NFT: {asset_name}" - )))?; + let nft_metadata = invoke_ctx.contract_context.meta_nft.get(asset_name).ok_or( + RuntimeCheckErrorKind::Unreachable(format!("No such NFT: {asset_name}")), + )?; let expected_asset_type = &nft_metadata.key_type; let asset_size = asset + .as_ref() .serialized_size() .map_err(|e| VmInternalError::Expect(e.to_string()))? as u64; - runtime_cost(ClarityCostFunction::NftMint, env, asset_size)?; + runtime_cost(ClarityCostFunction::NftMint, exec_state, asset_size)?; - if !expected_asset_type.admits(env.epoch(), &asset)? { + if !expected_asset_type.admits(exec_state.epoch(), asset.as_ref())? { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(expected_asset_type.clone()), - Box::new(asset), + asset.as_ref().to_error_string(), ) .into()); } - if let Value::Principal(ref to_principal) = to { - match env.global_context.database.get_nft_owner( - &env.contract_context.contract_identifier, + if let Value::Principal(to_principal) = to.as_ref() { + match exec_state.global_context.database.get_nft_owner( + &invoke_ctx.contract_context.contract_identifier, asset_name, - &asset, + asset.as_ref(), expected_asset_type, ) { Err(VmExecutionError::Runtime(RuntimeError::NoSuchToken, _)) => Ok(()), @@ -516,30 +539,31 @@ pub fn special_mint_asset_v205( Err(e) => Err(e), }?; - env.add_memory(TypeSignature::PrincipalType.size()?.into())?; - env.add_memory(asset_size)?; + exec_state.add_memory(TypeSignature::PrincipalType.size()?.into())?; + exec_state.add_memory(asset_size)?; - let epoch = *env.epoch(); - env.global_context.database.set_nft_owner( - &env.contract_context.contract_identifier, + let epoch = *exec_state.epoch(); + exec_state.global_context.database.set_nft_owner( + &invoke_ctx.contract_context.contract_identifier, asset_name, - &asset, + asset.as_ref(), to_principal, expected_asset_type, &epoch, )?; let asset_identifier = AssetIdentifier { - contract_identifier: env.contract_context.contract_identifier.clone(), + contract_identifier: invoke_ctx.contract_context.contract_identifier.clone(), asset_name: asset_name.clone(), }; - env.register_nft_mint_event(to_principal.clone(), asset, asset_identifier)?; + let asset = asset.clone_with_cost(exec_state)?; + exec_state.register_nft_mint_event(to_principal.clone(), asset, asset_identifier)?; Ok(Value::okay_true()) } else { Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::PrincipalType), - Box::new(to), + to.as_ref().to_error_string(), ) .into()) } @@ -547,7 +571,8 @@ pub fn special_mint_asset_v205( pub fn special_transfer_asset_v200( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(4, args)?; @@ -558,42 +583,40 @@ pub fn special_transfer_asset_v200( "Bad token name".to_string(), ))?; - let asset = eval(&args[1], env, context)?; - let from = eval(&args[2], env, context)?; - let to = eval(&args[3], env, context)?; - - let nft_metadata = - env.contract_context - .meta_nft - .get(asset_name) - .ok_or(RuntimeCheckErrorKind::Unreachable(format!( - "No such NFT: {asset_name}" - )))?; + let asset = eval(&args[1], exec_state, invoke_ctx, context)?; + let from = eval(&args[2], exec_state, invoke_ctx, context)?; + let to = eval(&args[3], exec_state, invoke_ctx, context)?; + + let nft_metadata = invoke_ctx.contract_context.meta_nft.get(asset_name).ok_or( + RuntimeCheckErrorKind::Unreachable(format!("No such NFT: {asset_name}")), + )?; let expected_asset_type = &nft_metadata.key_type; runtime_cost( ClarityCostFunction::NftTransfer, - env, + exec_state, expected_asset_type.size()?, )?; - if !expected_asset_type.admits(env.epoch(), &asset)? { + if !expected_asset_type.admits(exec_state.epoch(), asset.as_ref())? { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(expected_asset_type.clone()), - Box::new(asset), + asset.as_ref().to_error_string(), ) .into()); } - if let (Value::Principal(ref from_principal), Value::Principal(ref to_principal)) = (from, to) { + if let (Value::Principal(from_principal), Value::Principal(to_principal)) = + (from.as_ref(), to.as_ref()) + { if from_principal == to_principal { return clarity_ecode!(TransferAssetErrorCodes::SENDER_IS_RECIPIENT); } - let current_owner = match env.global_context.database.get_nft_owner( - &env.contract_context.contract_identifier, + let current_owner = match exec_state.global_context.database.get_nft_owner( + &invoke_ctx.contract_context.contract_identifier, asset_name, - &asset, + asset.as_ref(), expected_asset_type, ) { Ok(owner) => Ok(owner), @@ -607,31 +630,32 @@ pub fn special_transfer_asset_v200( return clarity_ecode!(TransferAssetErrorCodes::NOT_OWNED_BY); } - env.add_memory(TypeSignature::PrincipalType.size()?.into())?; - env.add_memory(expected_asset_type.size()?.into())?; + exec_state.add_memory(TypeSignature::PrincipalType.size()?.into())?; + exec_state.add_memory(expected_asset_type.size()?.into())?; - let epoch = *env.epoch(); - env.global_context.database.set_nft_owner( - &env.contract_context.contract_identifier, + let epoch = *exec_state.epoch(); + exec_state.global_context.database.set_nft_owner( + &invoke_ctx.contract_context.contract_identifier, asset_name, - &asset, + asset.as_ref(), to_principal, expected_asset_type, &epoch, )?; - env.global_context.log_asset_transfer( + let asset = asset.clone_with_cost(exec_state)?; + exec_state.global_context.log_asset_transfer( from_principal, - &env.contract_context.contract_identifier, + &invoke_ctx.contract_context.contract_identifier, asset_name, asset.clone(), )?; let asset_identifier = AssetIdentifier { - contract_identifier: env.contract_context.contract_identifier.clone(), + contract_identifier: invoke_ctx.contract_context.contract_identifier.clone(), asset_name: asset_name.clone(), }; - env.register_nft_transfer_event( + exec_state.register_nft_transfer_event( from_principal.clone(), to_principal.clone(), asset, @@ -648,7 +672,8 @@ pub fn special_transfer_asset_v200( /// asset as input to the cost tabulation. Otherwise identical to v200. pub fn special_transfer_asset_v205( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(4, args)?; @@ -659,41 +684,40 @@ pub fn special_transfer_asset_v205( "Bad token name".to_string(), ))?; - let asset = eval(&args[1], env, context)?; - let from = eval(&args[2], env, context)?; - let to = eval(&args[3], env, context)?; - - let nft_metadata = - env.contract_context - .meta_nft - .get(asset_name) - .ok_or(RuntimeCheckErrorKind::Unreachable(format!( - "No such NFT: {asset_name}" - )))?; + let asset = eval(&args[1], exec_state, invoke_ctx, context)?; + let from = eval(&args[2], exec_state, invoke_ctx, context)?; + let to = eval(&args[3], exec_state, invoke_ctx, context)?; + + let nft_metadata = invoke_ctx.contract_context.meta_nft.get(asset_name).ok_or( + RuntimeCheckErrorKind::Unreachable(format!("No such NFT: {asset_name}")), + )?; let expected_asset_type = &nft_metadata.key_type; let asset_size = asset + .as_ref() .serialized_size() .map_err(|e| VmInternalError::Expect(e.to_string()))? as u64; - runtime_cost(ClarityCostFunction::NftTransfer, env, asset_size)?; + runtime_cost(ClarityCostFunction::NftTransfer, exec_state, asset_size)?; - if !expected_asset_type.admits(env.epoch(), &asset)? { + if !expected_asset_type.admits(exec_state.epoch(), asset.as_ref())? { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(expected_asset_type.clone()), - Box::new(asset), + asset.as_ref().to_error_string(), ) .into()); } - if let (Value::Principal(ref from_principal), Value::Principal(ref to_principal)) = (from, to) { + if let (Value::Principal(from_principal), Value::Principal(to_principal)) = + (from.as_ref(), to.as_ref()) + { if from_principal == to_principal { return clarity_ecode!(TransferAssetErrorCodes::SENDER_IS_RECIPIENT); } - let current_owner = match env.global_context.database.get_nft_owner( - &env.contract_context.contract_identifier, + let current_owner = match exec_state.global_context.database.get_nft_owner( + &invoke_ctx.contract_context.contract_identifier, asset_name, - &asset, + asset.as_ref(), expected_asset_type, ) { Ok(owner) => Ok(owner), @@ -707,31 +731,32 @@ pub fn special_transfer_asset_v205( return clarity_ecode!(TransferAssetErrorCodes::NOT_OWNED_BY); } - env.add_memory(TypeSignature::PrincipalType.size()?.into())?; - env.add_memory(asset_size)?; + exec_state.add_memory(TypeSignature::PrincipalType.size()?.into())?; + exec_state.add_memory(asset_size)?; - let epoch = *env.epoch(); - env.global_context.database.set_nft_owner( - &env.contract_context.contract_identifier, + let epoch = *exec_state.epoch(); + exec_state.global_context.database.set_nft_owner( + &invoke_ctx.contract_context.contract_identifier, asset_name, - &asset, + asset.as_ref(), to_principal, expected_asset_type, &epoch, )?; - env.global_context.log_asset_transfer( + let asset = asset.clone_with_cost(exec_state)?; + exec_state.global_context.log_asset_transfer( from_principal, - &env.contract_context.contract_identifier, + &invoke_ctx.contract_context.contract_identifier, asset_name, asset.clone(), )?; let asset_identifier = AssetIdentifier { - contract_identifier: env.contract_context.contract_identifier.clone(), + contract_identifier: invoke_ctx.contract_context.contract_identifier.clone(), asset_name: asset_name.clone(), }; - env.register_nft_transfer_event( + exec_state.register_nft_transfer_event( from_principal.clone(), to_principal.clone(), asset, @@ -746,12 +771,13 @@ pub fn special_transfer_asset_v205( pub fn special_transfer_token( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(4, args)?; - runtime_cost(ClarityCostFunction::FtTransfer, env, 0)?; + runtime_cost(ClarityCostFunction::FtTransfer, exec_state, 0)?; let token_name = args[0] .match_atom() @@ -759,17 +785,14 @@ pub fn special_transfer_token( "Bad token name".to_string(), ))?; - let amount = eval(&args[1], env, context)?; - let from = eval(&args[2], env, context)?; - let to = eval(&args[3], env, context)?; + let amount = eval(&args[1], exec_state, invoke_ctx, context)?; + let from = eval(&args[2], exec_state, invoke_ctx, context)?; + let to = eval(&args[3], exec_state, invoke_ctx, context)?; - if let ( - Value::UInt(amount), - Value::Principal(ref from_principal), - Value::Principal(ref to_principal), - ) = (amount, from, to) + if let (Value::UInt(amount), Value::Principal(from_principal), Value::Principal(to_principal)) = + (amount.as_ref(), from.as_ref(), to.as_ref()) { - if amount == 0 { + if *amount == 0 { return clarity_ecode!(TransferTokenErrorCodes::NON_POSITIVE_AMOUNT); } @@ -777,25 +800,25 @@ pub fn special_transfer_token( return clarity_ecode!(TransferTokenErrorCodes::SENDER_IS_RECIPIENT); } - let ft_info = env.contract_context.meta_ft.get(token_name).ok_or( + let ft_info = invoke_ctx.contract_context.meta_ft.get(token_name).ok_or( RuntimeCheckErrorKind::Unreachable(format!("No such FT: {token_name}")), )?; - let from_bal = env.global_context.database.get_ft_balance( - &env.contract_context.contract_identifier, + let from_bal = exec_state.global_context.database.get_ft_balance( + &invoke_ctx.contract_context.contract_identifier, token_name, from_principal, Some(ft_info), )?; - if from_bal < amount { + if from_bal < *amount { return clarity_ecode!(TransferTokenErrorCodes::NOT_ENOUGH_BALANCE); } - let final_from_bal = from_bal - amount; + let final_from_bal = from_bal - *amount; - let to_bal = env.global_context.database.get_ft_balance( - &env.contract_context.contract_identifier, + let to_bal = exec_state.global_context.database.get_ft_balance( + &invoke_ctx.contract_context.contract_identifier, token_name, to_principal, Some(ft_info), @@ -804,42 +827,42 @@ pub fn special_transfer_token( // `ArithmeticOverflow` in this function is **unreachable** in normal Clarity execution because: // - the total liquid ustx supply will overflow before such an overflowing transfer is allowed. let final_to_bal = to_bal - .checked_add(amount) + .checked_add(*amount) .ok_or(RuntimeError::ArithmeticOverflow)?; - env.add_memory(TypeSignature::PrincipalType.size()?.into())?; - env.add_memory(TypeSignature::PrincipalType.size()?.into())?; - env.add_memory(TypeSignature::UIntType.size()?.into())?; - env.add_memory(TypeSignature::UIntType.size()?.into())?; + exec_state.add_memory(TypeSignature::PrincipalType.size()?.into())?; + exec_state.add_memory(TypeSignature::PrincipalType.size()?.into())?; + exec_state.add_memory(TypeSignature::UIntType.size()?.into())?; + exec_state.add_memory(TypeSignature::UIntType.size()?.into())?; - env.global_context.database.set_ft_balance( - &env.contract_context.contract_identifier, + exec_state.global_context.database.set_ft_balance( + &invoke_ctx.contract_context.contract_identifier, token_name, from_principal, final_from_bal, )?; - env.global_context.database.set_ft_balance( - &env.contract_context.contract_identifier, + exec_state.global_context.database.set_ft_balance( + &invoke_ctx.contract_context.contract_identifier, token_name, to_principal, final_to_bal, )?; - env.global_context.log_token_transfer( + exec_state.global_context.log_token_transfer( from_principal, - &env.contract_context.contract_identifier, + &invoke_ctx.contract_context.contract_identifier, token_name, - amount, + *amount, )?; let asset_identifier = AssetIdentifier { - contract_identifier: env.contract_context.contract_identifier.clone(), + contract_identifier: invoke_ctx.contract_context.contract_identifier.clone(), asset_name: token_name.clone(), }; - env.register_ft_transfer_event( + exec_state.register_ft_transfer_event( from_principal.clone(), to_principal.clone(), - amount, + *amount, asset_identifier, )?; @@ -851,12 +874,13 @@ pub fn special_transfer_token( pub fn special_get_balance( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; - runtime_cost(ClarityCostFunction::FtBalance, env, 0)?; + runtime_cost(ClarityCostFunction::FtBalance, exec_state, 0)?; let token_name = args[0] .match_atom() @@ -864,15 +888,15 @@ pub fn special_get_balance( "Bad token name".to_string(), ))?; - let owner = eval(&args[1], env, context)?; + let owner = eval(&args[1], exec_state, invoke_ctx, context)?; - if let Value::Principal(ref principal) = owner { - let ft_info = env.contract_context.meta_ft.get(token_name).ok_or( + if let Value::Principal(principal) = owner.as_ref() { + let ft_info = invoke_ctx.contract_context.meta_ft.get(token_name).ok_or( RuntimeCheckErrorKind::Unreachable(format!("No such FT: {token_name}")), )?; - let balance = env.global_context.database.get_ft_balance( - &env.contract_context.contract_identifier, + let balance = exec_state.global_context.database.get_ft_balance( + &invoke_ctx.contract_context.contract_identifier, token_name, principal, Some(ft_info), @@ -881,7 +905,7 @@ pub fn special_get_balance( } else { Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::PrincipalType), - Box::new(owner), + owner.as_ref().to_error_string(), ) .into()) } @@ -889,7 +913,8 @@ pub fn special_get_balance( pub fn special_get_owner_v200( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; @@ -900,35 +925,31 @@ pub fn special_get_owner_v200( "Bad token name".to_string(), ))?; - let asset = eval(&args[1], env, context)?; + let asset = eval(&args[1], exec_state, invoke_ctx, context)?; - let nft_metadata = - env.contract_context - .meta_nft - .get(asset_name) - .ok_or(RuntimeCheckErrorKind::Unreachable(format!( - "No such NFT: {asset_name}" - )))?; + let nft_metadata = invoke_ctx.contract_context.meta_nft.get(asset_name).ok_or( + RuntimeCheckErrorKind::Unreachable(format!("No such NFT: {asset_name}")), + )?; let expected_asset_type = &nft_metadata.key_type; runtime_cost( ClarityCostFunction::NftOwner, - env, + exec_state, expected_asset_type.size()?, )?; - if !expected_asset_type.admits(env.epoch(), &asset)? { + if !expected_asset_type.admits(exec_state.epoch(), asset.as_ref())? { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(expected_asset_type.clone()), - Box::new(asset), + asset.as_ref().to_error_string(), ) .into()); } - match env.global_context.database.get_nft_owner( - &env.contract_context.contract_identifier, + match exec_state.global_context.database.get_nft_owner( + &invoke_ctx.contract_context.contract_identifier, asset_name, - &asset, + asset.as_ref(), expected_asset_type, ) { Ok(owner) => Ok(Value::some(Value::Principal(owner)).map_err(|_| { @@ -943,7 +964,8 @@ pub fn special_get_owner_v200( /// asset as input to the cost tabulation. Otherwise identical to v200. pub fn special_get_owner_v205( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; @@ -954,34 +976,31 @@ pub fn special_get_owner_v205( "Bad token name".to_string(), ))?; - let asset = eval(&args[1], env, context)?; + let asset = eval(&args[1], exec_state, invoke_ctx, context)?; - let nft_metadata = - env.contract_context - .meta_nft - .get(asset_name) - .ok_or(RuntimeCheckErrorKind::Unreachable(format!( - "No such NFT: {asset_name}" - )))?; + let nft_metadata = invoke_ctx.contract_context.meta_nft.get(asset_name).ok_or( + RuntimeCheckErrorKind::Unreachable(format!("No such NFT: {asset_name}")), + )?; let expected_asset_type = &nft_metadata.key_type; let asset_size = asset + .as_ref() .serialized_size() .map_err(|e| VmInternalError::Expect(e.to_string()))? as u64; - runtime_cost(ClarityCostFunction::NftOwner, env, asset_size)?; + runtime_cost(ClarityCostFunction::NftOwner, exec_state, asset_size)?; - if !expected_asset_type.admits(env.epoch(), &asset)? { + if !expected_asset_type.admits(exec_state.epoch(), asset.as_ref())? { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(expected_asset_type.clone()), - Box::new(asset), + asset.as_ref().to_error_string(), ) .into()); } - match env.global_context.database.get_nft_owner( - &env.contract_context.contract_identifier, + match exec_state.global_context.database.get_nft_owner( + &invoke_ctx.contract_context.contract_identifier, asset_name, - &asset, + asset.as_ref(), expected_asset_type, ) { Ok(owner) => Ok(Value::some(Value::Principal(owner)).map_err(|_| { @@ -994,12 +1013,13 @@ pub fn special_get_owner_v205( pub fn special_get_token_supply( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, _context: &LocalContext, ) -> Result { check_argument_count(1, args)?; - runtime_cost(ClarityCostFunction::FtSupply, env, 0)?; + runtime_cost(ClarityCostFunction::FtSupply, exec_state, 0)?; let token_name = args[0] .match_atom() @@ -1007,21 +1027,22 @@ pub fn special_get_token_supply( "Bad token name".to_string(), ))?; - let supply = env + let supply = exec_state .global_context .database - .get_ft_supply(&env.contract_context.contract_identifier, token_name)?; + .get_ft_supply(&invoke_ctx.contract_context.contract_identifier, token_name)?; Ok(Value::UInt(supply)) } pub fn special_burn_token( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(3, args)?; - runtime_cost(ClarityCostFunction::FtBurn, env, 0)?; + runtime_cost(ClarityCostFunction::FtBurn, exec_state, 0)?; let token_name = args[0] .match_atom() @@ -1029,54 +1050,57 @@ pub fn special_burn_token( "Bad token name".to_string(), ))?; - let amount = eval(&args[1], env, context)?; - let from = eval(&args[2], env, context)?; + let amount = eval(&args[1], exec_state, invoke_ctx, context)?; + let from = eval(&args[2], exec_state, invoke_ctx, context)?; - if let (Value::UInt(amount), Value::Principal(ref burner)) = (amount, from) { - if amount == 0 { + if let (Value::UInt(amount), Value::Principal(burner)) = (amount.as_ref(), from.as_ref()) { + if *amount == 0 { return clarity_ecode!(BurnTokenErrorCodes::NOT_ENOUGH_BALANCE_OR_NON_POSITIVE); } - let burner_bal = env.global_context.database.get_ft_balance( - &env.contract_context.contract_identifier, + let burner_bal = exec_state.global_context.database.get_ft_balance( + &invoke_ctx.contract_context.contract_identifier, token_name, burner, None, )?; - if amount > burner_bal { + if *amount > burner_bal { return clarity_ecode!(BurnTokenErrorCodes::NOT_ENOUGH_BALANCE_OR_NON_POSITIVE); } - env.global_context.database.checked_decrease_token_supply( - &env.contract_context.contract_identifier, - token_name, - amount, - )?; + exec_state + .global_context + .database + .checked_decrease_token_supply( + &invoke_ctx.contract_context.contract_identifier, + token_name, + *amount, + )?; let final_burner_bal = burner_bal - amount; - env.global_context.database.set_ft_balance( - &env.contract_context.contract_identifier, + exec_state.global_context.database.set_ft_balance( + &invoke_ctx.contract_context.contract_identifier, token_name, burner, final_burner_bal, )?; let asset_identifier = AssetIdentifier { - contract_identifier: env.contract_context.contract_identifier.clone(), + contract_identifier: invoke_ctx.contract_context.contract_identifier.clone(), asset_name: token_name.clone(), }; - env.register_ft_burn_event(burner.clone(), amount, asset_identifier)?; + exec_state.register_ft_burn_event(burner.clone(), *amount, asset_identifier)?; - env.add_memory(TypeSignature::PrincipalType.size()?.into())?; - env.add_memory(TypeSignature::UIntType.size()?.into())?; + exec_state.add_memory(TypeSignature::PrincipalType.size()?.into())?; + exec_state.add_memory(TypeSignature::UIntType.size()?.into())?; - env.global_context.log_token_transfer( + exec_state.global_context.log_token_transfer( burner, - &env.contract_context.contract_identifier, + &invoke_ctx.contract_context.contract_identifier, token_name, - amount, + *amount, )?; Ok(Value::okay_true()) @@ -1087,12 +1111,13 @@ pub fn special_burn_token( pub fn special_burn_asset_v200( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(3, args)?; - runtime_cost(ClarityCostFunction::NftBurn, env, 0)?; + runtime_cost(ClarityCostFunction::NftBurn, exec_state, 0)?; let asset_name = args[0] .match_atom() @@ -1100,37 +1125,33 @@ pub fn special_burn_asset_v200( "Bad token name".to_string(), ))?; - let asset = eval(&args[1], env, context)?; - let sender = eval(&args[2], env, context)?; + let asset = eval(&args[1], exec_state, invoke_ctx, context)?; + let sender = eval(&args[2], exec_state, invoke_ctx, context)?; - let nft_metadata = - env.contract_context - .meta_nft - .get(asset_name) - .ok_or(RuntimeCheckErrorKind::Unreachable(format!( - "No such NFT: {asset_name}" - )))?; + let nft_metadata = invoke_ctx.contract_context.meta_nft.get(asset_name).ok_or( + RuntimeCheckErrorKind::Unreachable(format!("No such NFT: {asset_name}")), + )?; let expected_asset_type = &nft_metadata.key_type; runtime_cost( ClarityCostFunction::NftBurn, - env, + exec_state, expected_asset_type.size()?, )?; - if !expected_asset_type.admits(env.epoch(), &asset)? { + if !expected_asset_type.admits(exec_state.epoch(), asset.as_ref())? { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(expected_asset_type.clone()), - Box::new(asset), + asset.as_ref().to_error_string(), ) .into()); } - if let Value::Principal(ref sender_principal) = sender { - let owner = match env.global_context.database.get_nft_owner( - &env.contract_context.contract_identifier, + if let Value::Principal(sender_principal) = sender.as_ref() { + let owner = match exec_state.global_context.database.get_nft_owner( + &invoke_ctx.contract_context.contract_identifier, asset_name, - &asset, + asset.as_ref(), expected_asset_type, ) { Err(VmExecutionError::Runtime(RuntimeError::NoSuchToken, _)) => { @@ -1144,36 +1165,37 @@ pub fn special_burn_asset_v200( return clarity_ecode!(BurnAssetErrorCodes::NOT_OWNED_BY); } - env.add_memory(TypeSignature::PrincipalType.size()?.into())?; - env.add_memory(expected_asset_type.size()?.into())?; + exec_state.add_memory(TypeSignature::PrincipalType.size()?.into())?; + exec_state.add_memory(expected_asset_type.size()?.into())?; - let epoch = *env.epoch(); - env.global_context.database.burn_nft( - &env.contract_context.contract_identifier, + let epoch = *exec_state.epoch(); + exec_state.global_context.database.burn_nft( + &invoke_ctx.contract_context.contract_identifier, asset_name, - &asset, + asset.as_ref(), expected_asset_type, &epoch, )?; - env.global_context.log_asset_transfer( + let asset = asset.clone_with_cost(exec_state)?; + exec_state.global_context.log_asset_transfer( sender_principal, - &env.contract_context.contract_identifier, + &invoke_ctx.contract_context.contract_identifier, asset_name, asset.clone(), )?; let asset_identifier = AssetIdentifier { - contract_identifier: env.contract_context.contract_identifier.clone(), + contract_identifier: invoke_ctx.contract_context.contract_identifier.clone(), asset_name: asset_name.clone(), }; - env.register_nft_burn_event(sender_principal.clone(), asset, asset_identifier)?; + exec_state.register_nft_burn_event(sender_principal.clone(), asset, asset_identifier)?; Ok(Value::okay_true()) } else { Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::PrincipalType), - Box::new(sender), + sender.as_ref().to_error_string(), ) .into()) } @@ -1183,12 +1205,13 @@ pub fn special_burn_asset_v200( /// asset as input to the cost tabulation. Otherwise identical to v200. pub fn special_burn_asset_v205( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(3, args)?; - runtime_cost(ClarityCostFunction::NftBurn, env, 0)?; + runtime_cost(ClarityCostFunction::NftBurn, exec_state, 0)?; let asset_name = args[0] .match_atom() @@ -1196,36 +1219,33 @@ pub fn special_burn_asset_v205( "Bad token name".to_string(), ))?; - let asset = eval(&args[1], env, context)?; - let sender = eval(&args[2], env, context)?; + let asset = eval(&args[1], exec_state, invoke_ctx, context)?; + let sender = eval(&args[2], exec_state, invoke_ctx, context)?; - let nft_metadata = - env.contract_context - .meta_nft - .get(asset_name) - .ok_or(RuntimeCheckErrorKind::Unreachable(format!( - "No such NFT: {asset_name}" - )))?; + let nft_metadata = invoke_ctx.contract_context.meta_nft.get(asset_name).ok_or( + RuntimeCheckErrorKind::Unreachable(format!("No such NFT: {asset_name}")), + )?; let expected_asset_type = &nft_metadata.key_type; let asset_size = asset + .as_ref() .serialized_size() .map_err(|e| VmInternalError::Expect(e.to_string()))? as u64; - runtime_cost(ClarityCostFunction::NftBurn, env, asset_size)?; + runtime_cost(ClarityCostFunction::NftBurn, exec_state, asset_size)?; - if !expected_asset_type.admits(env.epoch(), &asset)? { + if !expected_asset_type.admits(exec_state.epoch(), asset.as_ref())? { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(expected_asset_type.clone()), - Box::new(asset), + asset.as_ref().to_error_string(), ) .into()); } - if let Value::Principal(ref sender_principal) = sender { - let owner = match env.global_context.database.get_nft_owner( - &env.contract_context.contract_identifier, + if let Value::Principal(sender_principal) = sender.as_ref() { + let owner = match exec_state.global_context.database.get_nft_owner( + &invoke_ctx.contract_context.contract_identifier, asset_name, - &asset, + asset.as_ref(), expected_asset_type, ) { Err(VmExecutionError::Runtime(RuntimeError::NoSuchToken, _)) => { @@ -1239,36 +1259,37 @@ pub fn special_burn_asset_v205( return clarity_ecode!(BurnAssetErrorCodes::NOT_OWNED_BY); } - env.add_memory(TypeSignature::PrincipalType.size()?.into())?; - env.add_memory(asset_size)?; + exec_state.add_memory(TypeSignature::PrincipalType.size()?.into())?; + exec_state.add_memory(asset_size)?; - let epoch = *env.epoch(); - env.global_context.database.burn_nft( - &env.contract_context.contract_identifier, + let epoch = *exec_state.epoch(); + exec_state.global_context.database.burn_nft( + &invoke_ctx.contract_context.contract_identifier, asset_name, - &asset, + asset.as_ref(), expected_asset_type, &epoch, )?; - env.global_context.log_asset_transfer( + let asset = asset.clone_with_cost(exec_state)?; + exec_state.global_context.log_asset_transfer( sender_principal, - &env.contract_context.contract_identifier, + &invoke_ctx.contract_context.contract_identifier, asset_name, asset.clone(), )?; let asset_identifier = AssetIdentifier { - contract_identifier: env.contract_context.contract_identifier.clone(), + contract_identifier: invoke_ctx.contract_context.contract_identifier.clone(), asset_name: asset_name.clone(), }; - env.register_nft_burn_event(sender_principal.clone(), asset, asset_identifier)?; + exec_state.register_nft_burn_event(sender_principal.clone(), asset, asset_identifier)?; Ok(Value::okay_true()) } else { Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::PrincipalType), - Box::new(sender), + sender.as_ref().to_error_string(), ) .into()) } diff --git a/clarity/src/vm/functions/boolean.rs b/clarity/src/vm/functions/boolean.rs index f20eeec9e31..b8b65b8c5da 100644 --- a/clarity/src/vm/functions/boolean.rs +++ b/clarity/src/vm/functions/boolean.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::vm::contexts::{Environment, LocalContext}; +use crate::vm::contexts::{ExecutionState, InvocationContext, LocalContext}; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::runtime_cost; use crate::vm::errors::{RuntimeCheckErrorKind, VmExecutionError, check_arguments_at_least}; @@ -27,23 +27,24 @@ fn type_force_bool(value: &Value) -> Result { Value::Bool(boolean) => Ok(boolean), _ => Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BoolType), - Box::new(value.clone()), + value.to_error_string(), )), } } pub fn special_or( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_arguments_at_least(1, args)?; - runtime_cost(ClarityCostFunction::Or, env, args.len())?; + runtime_cost(ClarityCostFunction::Or, exec_state, args.len())?; for arg in args.iter() { - let evaluated = eval(arg, env, context)?; - let result = type_force_bool(&evaluated)?; + let evaluated = eval(arg, exec_state, invoke_ctx, context)?; + let result = type_force_bool(evaluated.as_ref())?; if result { return Ok(Value::Bool(true)); } @@ -54,16 +55,17 @@ pub fn special_or( pub fn special_and( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_arguments_at_least(1, args)?; - runtime_cost(ClarityCostFunction::And, env, args.len())?; + runtime_cost(ClarityCostFunction::And, exec_state, args.len())?; for arg in args.iter() { - let evaluated = eval(arg, env, context)?; - let result = type_force_bool(&evaluated)?; + let evaluated = eval(arg, exec_state, invoke_ctx, context)?; + let result = type_force_bool(evaluated.as_ref())?; if !result { return Ok(Value::Bool(false)); } diff --git a/clarity/src/vm/functions/conversions.rs b/clarity/src/vm/functions/conversions.rs index 7d1698250c7..8212405d349 100644 --- a/clarity/src/vm/functions/conversions.rs +++ b/clarity/src/vm/functions/conversions.rs @@ -17,6 +17,7 @@ use clarity_types::errors::ClarityTypeError; use clarity_types::types::serialization::SerializationError; +use crate::vm::contexts::{ExecutionState, InvocationContext}; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::runtime_cost; use crate::vm::errors::{ @@ -29,7 +30,7 @@ use crate::vm::types::{ ASCIIData, BufferLength, CharType, SequenceData, TypeSignature, TypeSignatureExt as _, UTF8Data, Value, }; -use crate::vm::{Environment, LocalContext, eval}; +use crate::vm::{LocalContext, eval}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum EndianDirection { @@ -64,7 +65,7 @@ pub fn buff_to_int_generic( BufferLength::try_from(16_u32) .map_err(|_| VmInternalError::Expect("Failed to construct".into()))?, ))), - Box::new(value), + value.to_error_string(), ) .into()) } else { @@ -90,7 +91,7 @@ pub fn buff_to_int_generic( BufferLength::try_from(16_u32) .map_err(|_| VmInternalError::Expect("Failed to construct".into()))?, ))), - Box::new(value), + value.to_error_string(), ) .into()), } @@ -156,7 +157,7 @@ pub fn native_string_to_int_generic( TypeSignature::STRING_ASCII_MAX, TypeSignature::STRING_UTF8_MAX, ], - Box::new(value), + value.to_error_string(), ) .into()), } @@ -209,7 +210,7 @@ pub fn native_int_to_string_generic( } _ => Err(RuntimeCheckErrorKind::UnionTypeValueError( vec![TypeSignature::IntType, TypeSignature::UIntType], - Box::new(value), + value.to_error_string(), ) .into()), } @@ -244,19 +245,24 @@ fn convert_utf8_to_ascii(s: String) -> Result { pub fn special_to_ascii( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(1, args)?; - let value = eval(&args[0], env, context)?; + let value = eval(&args[0], exec_state, invoke_ctx, context)?; - runtime_cost(ClarityCostFunction::ToAscii, env, value.size()?)?; + runtime_cost( + ClarityCostFunction::ToAscii, + exec_state, + value.as_ref().size()?, + )?; - match value { + match value.as_ref() { Value::Int(num) => convert_string_to_ascii_ok(num.to_string()), Value::UInt(num) => convert_string_to_ascii_ok(format!("u{num}")), - Value::Bool(b) => convert_string_to_ascii_ok(if b { + Value::Bool(b) => convert_string_to_ascii_ok(if *b { "true".to_string() } else { "false".to_string() @@ -266,8 +272,7 @@ pub fn special_to_ascii( convert_string_to_ascii_ok(format!("0x{buffer_data}")) } Value::Sequence(SequenceData::String(CharType::UTF8(UTF8Data { data }))) => { - // Convert UTF8 to string first, then to ASCII - let flattened_bytes: Vec = data.into_iter().flatten().collect(); + let flattened_bytes: Vec = data.iter().flatten().copied().collect(); match String::from_utf8(flattened_bytes) { Ok(utf8_string) => Ok(convert_utf8_to_ascii(utf8_string)?), Err(_) => Ok(Value::err_uint(1)), // Invalid UTF8 @@ -282,7 +287,7 @@ pub fn special_to_ascii( TypeSignature::TO_ASCII_BUFFER_MAX, TypeSignature::STRING_UTF8_MAX, ], - Box::new(value), + value.as_ref().to_error_string(), ) .into()), } @@ -313,26 +318,27 @@ pub fn to_consensus_buff(value: Value) -> Result { /// to an unexpected type, returns `none`. Otherwise, it will be `(some value)` pub fn from_consensus_buff( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; - let type_arg = TypeSignature::parse_type_repr(*env.epoch(), &args[0], env)?; - let value = eval(&args[1], env, context)?; + let type_arg = TypeSignature::parse_type_repr(*exec_state.epoch(), &args[0], exec_state)?; + let value = eval(&args[1], exec_state, invoke_ctx, context)?; // get the buffer bytes from the supplied value. if not passed a buffer, // this is a type error - let input_bytes = if let Value::Sequence(SequenceData::Buffer(buff_data)) = value { - Ok(buff_data.data) + let input_bytes = if let Value::Sequence(SequenceData::Buffer(buff_data)) = value.as_ref() { + Ok(&buff_data.data) } else { Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_MAX), - Box::new(value), + value.as_ref().to_error_string(), )) }?; - let input = if env + let input = if invoke_ctx .contract_context .get_clarity_version() .protects_logn_cost_fn() @@ -341,19 +347,19 @@ pub fn from_consensus_buff( } else { input_bytes.len() }; - runtime_cost(ClarityCostFunction::FromConsensusBuff, env, input)?; + runtime_cost(ClarityCostFunction::FromConsensusBuff, exec_state, input)?; // Perform the deserialization and check that it deserialized to the expected // type. A type mismatch at this point is an error that should be surfaced in // Clarity (as a none return). let result = match Value::try_deserialize_bytes_exact( - &input_bytes, + input_bytes, &type_arg, - env.epoch().value_sanitizing(), + exec_state.epoch().value_sanitizing(), ) { Ok(value) => value, Err(SerializationError::UnexpectedSerialization) => { - if env.epoch().treats_unexpected_serialization_as_none() { + if exec_state.epoch().treats_unexpected_serialization_as_none() { return Ok(Value::none()); } return Err( @@ -362,7 +368,7 @@ pub fn from_consensus_buff( } Err(_) => return Ok(Value::none()), }; - if !type_arg.admits(env.epoch(), &result)? { + if !type_arg.admits(exec_state.epoch(), &result)? { return Ok(Value::none()); } diff --git a/clarity/src/vm/functions/crypto.rs b/clarity/src/vm/functions/crypto.rs index 4495550642b..fab77e0550f 100644 --- a/clarity/src/vm/functions/crypto.rs +++ b/clarity/src/vm/functions/crypto.rs @@ -22,6 +22,7 @@ use stacks_common::util::hash; use stacks_common::util::secp256k1::{Secp256k1PublicKey, secp256k1_recover, secp256k1_verify}; use stacks_common::util::secp256r1::{secp256r1_verify, secp256r1_verify_digest}; +use crate::vm::contexts::{ExecutionState, InvocationContext}; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::runtime_cost; use crate::vm::errors::{ @@ -29,7 +30,7 @@ use crate::vm::errors::{ }; use crate::vm::representations::SymbolicExpression; use crate::vm::types::{BuffData, SequenceData, TypeSignature, Value}; -use crate::vm::{ClarityVersion, Environment, LocalContext, eval}; +use crate::vm::{ClarityVersion, LocalContext, eval}; macro_rules! native_hash_func { ($name:ident, $module:ty) => { @@ -44,7 +45,7 @@ macro_rules! native_hash_func { TypeSignature::UIntType, TypeSignature::BUFFER_MAX, ], - Box::new(input), + input.to_error_string(), )), }?; let hash = <$module>::from_data(&bytes); @@ -94,31 +95,23 @@ fn pubkey_to_address_v2( pub fn special_principal_of( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { // (principal-of? (..)) // arg0 => (buff 33) check_argument_count(1, args)?; - runtime_cost(ClarityCostFunction::PrincipalOf, env, 0)?; + runtime_cost(ClarityCostFunction::PrincipalOf, exec_state, 0)?; - let param0 = eval(&args[0], env, context)?; - let pub_key = match param0 { - Value::Sequence(SequenceData::Buffer(BuffData { ref data })) => { - if data.len() != 33 { - return Err(RuntimeCheckErrorKind::TypeValueError( - Box::new(TypeSignature::BUFFER_33), - Box::new(param0), - ) - .into()); - } - data - } + let param0 = eval(&args[0], exec_state, invoke_ctx, context)?; + let pub_key = match param0.as_ref() { + Value::Sequence(SequenceData::Buffer(BuffData { data })) if data.len() == 33 => data, _ => { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_33), - Box::new(param0), + param0.as_ref().to_error_string(), ) .into()); } @@ -127,8 +120,9 @@ pub fn special_principal_of( if let Ok(pub_key) = Secp256k1PublicKey::from_slice(pub_key) { // Note: Clarity1 had a bug in how the address is computed (issues/2619). // We want to preserve the old behavior unless the version is greater. - let addr = if *env.contract_context.get_clarity_version() > ClarityVersion::Clarity1 { - pubkey_to_address_v2(pub_key, env.global_context.mainnet)? + let addr = if *invoke_ctx.contract_context.get_clarity_version() > ClarityVersion::Clarity1 + { + pubkey_to_address_v2(pub_key, exec_state.global_context.mainnet)? } else { pubkey_to_address_v1(pub_key)? }; @@ -142,43 +136,35 @@ pub fn special_principal_of( pub fn special_secp256k1_recover( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { // (secp256k1-recover? (..)) // arg0 => (buff 32), arg1 => (buff 65) check_argument_count(2, args)?; - runtime_cost(ClarityCostFunction::Secp256k1recover, env, 0)?; + runtime_cost(ClarityCostFunction::Secp256k1recover, exec_state, 0)?; - let param0 = eval(&args[0], env, context)?; - let message = match param0 { - Value::Sequence(SequenceData::Buffer(BuffData { ref data })) => { - if data.len() != 32 { - return Err(RuntimeCheckErrorKind::TypeValueError( - Box::new(TypeSignature::BUFFER_32), - Box::new(param0), - ) - .into()); - } - data - } + let param0 = eval(&args[0], exec_state, invoke_ctx, context)?; + let message = match param0.as_ref() { + Value::Sequence(SequenceData::Buffer(BuffData { data })) if data.len() == 32 => data, _ => { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_32), - Box::new(param0), + param0.as_ref().to_error_string(), ) .into()); } }; - let param1 = eval(&args[1], env, context)?; - let signature = match param1 { - Value::Sequence(SequenceData::Buffer(BuffData { ref data })) => { + let param1 = eval(&args[1], exec_state, invoke_ctx, context)?; + let signature = match param1.as_ref() { + Value::Sequence(SequenceData::Buffer(BuffData { data })) => { if data.len() > 65 { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_65), - Box::new(param1), + param1.as_ref().to_error_string(), ) .into()); } @@ -190,7 +176,7 @@ pub fn special_secp256k1_recover( _ => { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_65), - Box::new(param1), + param1.as_ref().to_error_string(), ) .into()); } @@ -208,43 +194,35 @@ pub fn special_secp256k1_recover( pub fn special_secp256k1_verify( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { // (secp256k1-verify (..)) // arg0 => (buff 32), arg1 => (buff 65), arg2 => (buff 33) check_argument_count(3, args)?; - runtime_cost(ClarityCostFunction::Secp256k1verify, env, 0)?; + runtime_cost(ClarityCostFunction::Secp256k1verify, exec_state, 0)?; - let param0 = eval(&args[0], env, context)?; - let message = match param0 { - Value::Sequence(SequenceData::Buffer(BuffData { ref data })) => { - if data.len() != 32 { - return Err(RuntimeCheckErrorKind::TypeValueError( - Box::new(TypeSignature::BUFFER_32), - Box::new(param0), - ) - .into()); - } - data - } + let param0 = eval(&args[0], exec_state, invoke_ctx, context)?; + let message = match param0.as_ref() { + Value::Sequence(SequenceData::Buffer(BuffData { data })) if data.len() == 32 => data, _ => { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_32), - Box::new(param0), + param0.as_ref().to_error_string(), ) .into()); } }; - let param1 = eval(&args[1], env, context)?; - let signature = match param1 { - Value::Sequence(SequenceData::Buffer(BuffData { ref data })) => { + let param1 = eval(&args[1], exec_state, invoke_ctx, context)?; + let signature = match param1.as_ref() { + Value::Sequence(SequenceData::Buffer(BuffData { data })) => { if data.len() > 65 { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_65), - Box::new(param1), + param1.as_ref().to_error_string(), ) .into()); } @@ -259,28 +237,19 @@ pub fn special_secp256k1_verify( _ => { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_65), - Box::new(param1), + param1.as_ref().to_error_string(), ) .into()); } }; - let param2 = eval(&args[2], env, context)?; - let pubkey = match param2 { - Value::Sequence(SequenceData::Buffer(BuffData { ref data })) => { - if data.len() != 33 { - return Err(RuntimeCheckErrorKind::TypeValueError( - Box::new(TypeSignature::BUFFER_33), - Box::new(param2), - ) - .into()); - } - data - } + let param2 = eval(&args[2], exec_state, invoke_ctx, context)?; + let pubkey = match param2.as_ref() { + Value::Sequence(SequenceData::Buffer(BuffData { data })) if data.len() == 33 => data, _ => { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_33), - Box::new(param2), + param2.as_ref().to_error_string(), ) .into()); } @@ -293,34 +262,26 @@ pub fn special_secp256k1_verify( pub fn special_secp256r1_verify( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { // (secp256r1-verify message-hash signature public-key) // message-hash: (buff 32), signature: (buff 64), public-key: (buff 33) check_argument_count(3, args)?; - runtime_cost(ClarityCostFunction::Secp256r1verify, env, 0)?; + runtime_cost(ClarityCostFunction::Secp256r1verify, exec_state, 0)?; let arg0 = args .first() .ok_or(RuntimeCheckErrorKind::IncorrectArgumentCount(0, 3))?; - let message_value = eval(arg0, env, context)?; - let message = match message_value { - Value::Sequence(SequenceData::Buffer(BuffData { ref data })) => { - if data.len() != 32 { - return Err(RuntimeCheckErrorKind::TypeValueError( - Box::new(TypeSignature::BUFFER_32), - Box::new(message_value), - ) - .into()); - } - data - } + let message_value = eval(arg0, exec_state, invoke_ctx, context)?; + let message = match message_value.as_ref() { + Value::Sequence(SequenceData::Buffer(BuffData { data })) if data.len() == 32 => data, _ => { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_32), - Box::new(message_value), + message_value.as_ref().to_error_string(), ) .into()); } @@ -329,16 +290,9 @@ pub fn special_secp256r1_verify( let arg1 = args .get(1) .ok_or(RuntimeCheckErrorKind::IncorrectArgumentCount(1, 3))?; - let signature_value = eval(arg1, env, context)?; - let signature = match signature_value { - Value::Sequence(SequenceData::Buffer(BuffData { ref data })) => { - if data.len() > 64 { - return Err(RuntimeCheckErrorKind::TypeValueError( - Box::new(TypeSignature::BUFFER_64), - Box::new(signature_value), - ) - .into()); - } + let signature_value = eval(arg1, exec_state, invoke_ctx, context)?; + let signature = match signature_value.as_ref() { + Value::Sequence(SequenceData::Buffer(BuffData { data })) if data.len() <= 64 => { if data.len() != 64 { return Ok(Value::Bool(false)); } @@ -347,7 +301,7 @@ pub fn special_secp256r1_verify( _ => { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_64), - Box::new(signature_value), + signature_value.as_ref().to_error_string(), ) .into()); } @@ -356,28 +310,19 @@ pub fn special_secp256r1_verify( let arg2 = args .get(2) .ok_or(RuntimeCheckErrorKind::IncorrectArgumentCount(2, 3))?; - let pubkey_value = eval(arg2, env, context)?; - let pubkey = match pubkey_value { - Value::Sequence(SequenceData::Buffer(BuffData { ref data })) => { - if data.len() != 33 { - return Err(RuntimeCheckErrorKind::TypeValueError( - Box::new(TypeSignature::BUFFER_33), - Box::new(pubkey_value), - ) - .into()); - } - data - } + let pubkey_value = eval(arg2, exec_state, invoke_ctx, context)?; + let pubkey = match pubkey_value.as_ref() { + Value::Sequence(SequenceData::Buffer(BuffData { data })) if data.len() == 33 => data, _ => { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_33), - Box::new(pubkey_value), + pubkey_value.as_ref().to_error_string(), ) .into()); } }; - let version = *env.contract_context.get_clarity_version(); + let version = *invoke_ctx.contract_context.get_clarity_version(); let verify_result = if version.uses_secp256r1_double_hashing() { secp256r1_verify(message, signature, pubkey) } else { diff --git a/clarity/src/vm/functions/database.rs b/clarity/src/vm/functions/database.rs index 7853f8de1f6..5db19ed290c 100644 --- a/clarity/src/vm/functions/database.rs +++ b/clarity/src/vm/functions/database.rs @@ -20,6 +20,7 @@ use stacks_common::types::StacksEpochId; use stacks_common::types::chainstate::StacksBlockId; use crate::vm::callables::DefineType; +use crate::vm::contexts::{ExecutionState, InvocationContext}; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::{CostTracker, MemoryConsumer, constants as cost_constants, runtime_cost}; use crate::vm::errors::{ @@ -31,7 +32,7 @@ use crate::vm::types::{ BlockInfoProperty, BuffData, BurnBlockInfoProperty, PrincipalData, SequenceData, StacksBlockInfoProperty, TenureInfoProperty, TupleData, TypeSignature, Value, }; -use crate::vm::{ClarityVersion, Environment, LocalContext, eval}; +use crate::vm::{ClarityVersion, LocalContext, eval}; switch_on_global_epoch!(special_fetch_variable( special_fetch_variable_v200, @@ -60,7 +61,8 @@ switch_on_global_epoch!(special_delete_entry( pub fn special_contract_call( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_arguments_at_least(2, args)?; @@ -68,7 +70,7 @@ pub fn special_contract_call( // the second part of the contract_call cost (i.e., the load contract cost) // is checked in `execute_contract`, and the function _application_ cost // is checked in callables::DefinedFunction::execute_apply. - runtime_cost(ClarityCostFunction::ContractCall, env, 0)?; + runtime_cost(ClarityCostFunction::ContractCall, exec_state, 0)?; let function_name = args[1] .match_atom() @@ -80,9 +82,11 @@ pub fn special_contract_call( let mut rest_args = Vec::with_capacity(rest_args_len); let mut rest_args_sizes = Vec::with_capacity(rest_args_len); for arg in rest_args_slice.iter() { - let evaluated_arg = eval(arg, env, context)?; - rest_args_sizes.push(evaluated_arg.size()?.into()); - rest_args.push(SymbolicExpression::atom_value(evaluated_arg)); + let evaluated_arg = eval(arg, exec_state, invoke_ctx, context)?; + rest_args_sizes.push(evaluated_arg.as_ref().size()?.into()); + rest_args.push(SymbolicExpression::atom_value( + evaluated_arg.clone_with_cost(exec_state)?, + )); } let (contract_identifier, type_returns_constraint) = match &args[0].expr { @@ -94,18 +98,34 @@ pub fn special_contract_call( } SymbolicExpressionType::Atom(contract_ref) => { // First, check if the atom references a contract constant which is a callable - let callable = env + let callable = invoke_ctx .contract_context .lookup_variable(contract_ref) .and_then(|value| { - if let Value::CallableContract(callable) = value { - Some(callable) - } else { - None + if !invoke_ctx + .contract_context + .get_clarity_version() + .supports_callables() + { + return None; + } + if !exec_state.epoch().supports_call_with_constant() { + return None; } + if invoke_ctx.contract_context.is_deploying { + return None; + } + let Value::Principal(PrincipalData::Contract(contract_identifier)) = value + else { + return None; + }; + Some(CallableData { + contract_identifier: contract_identifier.clone(), + trait_identifier: None, + }) }) // If not, check if the atom references a callable variable - .or_else(|| context.lookup_callable_contract(contract_ref)); + .or_else(|| context.lookup_callable_contract(contract_ref).cloned()); match callable { Some(CallableData { @@ -121,17 +141,17 @@ pub fn special_contract_call( trait_identifier: Some(trait_identifier), }) => { // Ensure that contract-call is used for inter-contract calls only - if contract_identifier == &env.contract_context.contract_identifier { + if contract_identifier == invoke_ctx.contract_context.contract_identifier { return Err(RuntimeCheckErrorKind::CircularReference(vec![ contract_identifier.name.to_string(), ]) .into()); } - let contract_to_check = env + let contract_to_check = exec_state .global_context .database - .get_contract(contract_identifier) + .get_contract(&contract_identifier) .map_err(|_e| { RuntimeCheckErrorKind::NoSuchContract(contract_identifier.to_string()) })?; @@ -140,14 +160,14 @@ pub fn special_contract_call( // Attempt to short circuit the dynamic dispatch checks: // If the contract is explicitely implementing the trait with `impl-trait`, // then we can simply rely on the analysis performed at publish time. - if contract_context_to_check.is_explicitly_implementing_trait(trait_identifier) + if contract_context_to_check.is_explicitly_implementing_trait(&trait_identifier) { (contract_identifier.clone(), None) } else { let trait_name = trait_identifier.name.to_string(); // Retrieve, from the trait definition, the expected method signature - let contract_defining_trait = env + let contract_defining_trait = exec_state .global_context .database .get_contract(&trait_identifier.contract_identifier) @@ -168,7 +188,7 @@ pub fn special_contract_call( ))?; // Check read/write compatibility - if env.global_context.is_read_only() { + if exec_state.global_context.is_read_only() { return Err(RuntimeCheckErrorKind::Unreachable( "Trait based contract call in read-only".to_string(), ) @@ -186,9 +206,9 @@ pub fn special_contract_call( // If this check succeeds, the subsequent trait reference and method checks cannot fail function_to_check.check_trait_expectations( - env.epoch(), + exec_state.epoch(), &contract_context_defining_trait, - trait_identifier, + &trait_identifier, )?; // Retrieve the expected method signature @@ -214,31 +234,47 @@ pub fn special_contract_call( _ => return Err(RuntimeCheckErrorKind::ContractCallExpectName.into()), }; - let contract_principal = env.contract_context.contract_identifier.clone().into(); + let contract_principal = invoke_ctx + .contract_context + .contract_identifier + .clone() + .into(); - let mut nested_env = env.nest_with_caller(contract_principal); - let result = if nested_env.short_circuit_contract_call( + let nested_ctx = invoke_ctx.with_caller(contract_principal); + let result = if exec_state.short_circuit_contract_call( &contract_identifier, function_name, &rest_args_sizes, )? { - nested_env.run_free(|free_env| { - free_env.execute_contract(&contract_identifier, function_name, &rest_args, false) + exec_state.run_free(&nested_ctx, |free_exec_state, nested_ctx| { + free_exec_state.execute_contract( + nested_ctx, + &contract_identifier, + function_name, + &rest_args, + false, + ) }) } else { - nested_env.execute_contract(&contract_identifier, function_name, &rest_args, false) + exec_state.execute_contract( + &nested_ctx, + &contract_identifier, + function_name, + &rest_args, + false, + ) }?; // sanitize contract-call outputs in epochs >= 2.4 let result_type = TypeSignature::type_of(&result)?; - let (result, _) = Value::sanitize_value(env.epoch(), &result_type, result) + let (result, _) = Value::sanitize_value(exec_state.epoch(), &result_type, result) .ok_or_else(|| RuntimeCheckErrorKind::CouldNotDetermineType)?; // Ensure that the expected type from the trait spec admits // the type of the value returned by the dynamic dispatch. if let Some(returns_type_signature) = type_returns_constraint { let actual_returns = TypeSignature::type_of(&result)?; - if !returns_type_signature.admits_type(env.epoch(), &actual_returns)? { + if !returns_type_signature.admits_type(exec_state.epoch(), &actual_returns)? { return Err(RuntimeCheckErrorKind::ReturnTypesMustMatch( Box::new(returns_type_signature), Box::new(actual_returns), @@ -252,7 +288,8 @@ pub fn special_contract_call( pub fn special_fetch_variable_v200( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, _context: &LocalContext, ) -> Result { check_argument_count(1, args)?; @@ -263,20 +300,25 @@ pub fn special_fetch_variable_v200( "Expected name".to_string(), ))?; - let contract = &env.contract_context.contract_identifier; + let contract = &invoke_ctx.contract_context.contract_identifier; - let data_types = env.contract_context.meta_data_var.get(var_name).ok_or( - RuntimeCheckErrorKind::Unreachable(format!("No such data variable: {var_name}")), - )?; + let data_types = invoke_ctx + .contract_context + .meta_data_var + .get(var_name) + .ok_or(RuntimeCheckErrorKind::Unreachable(format!( + "No such data variable: {var_name}" + )))?; runtime_cost( ClarityCostFunction::FetchVar, - env, + exec_state, data_types.value_type.size()?, )?; - let epoch = *env.epoch(); - env.global_context + let epoch = *exec_state.epoch(); + exec_state + .global_context .database .lookup_variable(contract, var_name, data_types, &epoch) } @@ -285,7 +327,8 @@ pub fn special_fetch_variable_v200( /// value as input to the cost tabulation. Otherwise identical to v200. pub fn special_fetch_variable_v205( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, _context: &LocalContext, ) -> Result { check_argument_count(1, args)?; @@ -296,14 +339,18 @@ pub fn special_fetch_variable_v205( "Expected name".to_string(), ))?; - let contract = &env.contract_context.contract_identifier; + let contract = &invoke_ctx.contract_context.contract_identifier; - let data_types = env.contract_context.meta_data_var.get(var_name).ok_or( - RuntimeCheckErrorKind::Unreachable(format!("No such data variable: {var_name}")), - )?; + let data_types = invoke_ctx + .contract_context + .meta_data_var + .get(var_name) + .ok_or(RuntimeCheckErrorKind::Unreachable(format!( + "No such data variable: {var_name}" + )))?; - let epoch = *env.epoch(); - let result = env + let epoch = *exec_state.epoch(); + let result = exec_state .global_context .database .lookup_variable_with_size(contract, var_name, data_types, &epoch); @@ -313,17 +360,18 @@ pub fn special_fetch_variable_v205( Err(_e) => data_types.value_type.size()?.into(), }; - runtime_cost(ClarityCostFunction::FetchVar, env, result_size)?; + runtime_cost(ClarityCostFunction::FetchVar, exec_state, result_size)?; result.map(|data| data.value) } pub fn special_set_variable_v200( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { - if env.global_context.is_read_only() { + if exec_state.global_context.is_read_only() { return Err( RuntimeCheckErrorKind::Unreachable("Write attempted in read-only".to_string()).into(), ); @@ -331,7 +379,7 @@ pub fn special_set_variable_v200( check_argument_count(2, args)?; - let value = eval(&args[1], env, context)?; + let value = eval(&args[1], exec_state, invoke_ctx, context)?; let var_name = args[0] .match_atom() @@ -339,22 +387,28 @@ pub fn special_set_variable_v200( "Expected name".to_string(), ))?; - let contract = &env.contract_context.contract_identifier; + let contract = &invoke_ctx.contract_context.contract_identifier; - let data_types = env.contract_context.meta_data_var.get(var_name).ok_or( - RuntimeCheckErrorKind::Unreachable(format!("No such data variable: {var_name}")), - )?; + let data_types = invoke_ctx + .contract_context + .meta_data_var + .get(var_name) + .ok_or(RuntimeCheckErrorKind::Unreachable(format!( + "No such data variable: {var_name}" + )))?; runtime_cost( ClarityCostFunction::SetVar, - env, + exec_state, data_types.value_type.size()?, )?; - env.add_memory(value.get_memory_use()?)?; + exec_state.add_memory(value.as_ref().get_memory_use()?)?; - let epoch = *env.epoch(); - env.global_context + let value = value.clone_with_cost(exec_state)?; + let epoch = *exec_state.epoch(); + exec_state + .global_context .database .set_variable(contract, var_name, value, data_types, &epoch) .map(|data| data.value) @@ -364,10 +418,11 @@ pub fn special_set_variable_v200( /// value as input to the cost tabulation. Otherwise identical to v200. pub fn special_set_variable_v205( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { - if env.global_context.is_read_only() { + if exec_state.global_context.is_read_only() { return Err( RuntimeCheckErrorKind::Unreachable("Write attempted in read-only".to_string()).into(), ); @@ -375,7 +430,7 @@ pub fn special_set_variable_v205( check_argument_count(2, args)?; - let value = eval(&args[1], env, context)?; + let value = eval(&args[1], exec_state, invoke_ctx, context)?; let var_name = args[0] .match_atom() @@ -383,14 +438,19 @@ pub fn special_set_variable_v205( "Expected name".to_string(), ))?; - let contract = &env.contract_context.contract_identifier; + let contract = &invoke_ctx.contract_context.contract_identifier; - let data_types = env.contract_context.meta_data_var.get(var_name).ok_or( - RuntimeCheckErrorKind::Unreachable(format!("No such data variable: {var_name}")), - )?; + let data_types = invoke_ctx + .contract_context + .meta_data_var + .get(var_name) + .ok_or(RuntimeCheckErrorKind::Unreachable(format!( + "No such data variable: {var_name}" + )))?; - let epoch = *env.epoch(); - let result = env + let value = value.clone_with_cost(exec_state)?; + let epoch = *exec_state.epoch(); + let result = exec_state .global_context .database .set_variable(contract, var_name, value, data_types, &epoch); @@ -400,16 +460,17 @@ pub fn special_set_variable_v205( Err(_e) => data_types.value_type.size()?.into(), }; - runtime_cost(ClarityCostFunction::SetVar, env, result_size)?; + runtime_cost(ClarityCostFunction::SetVar, exec_state, result_size)?; - env.add_memory(result_size)?; + exec_state.add_memory(result_size)?; result.map(|data| data.value) } pub fn special_fetch_entry_v200( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; @@ -420,31 +481,40 @@ pub fn special_fetch_entry_v200( "Expected name".to_string(), ))?; - let key = eval(&args[1], env, context)?; + let key = eval(&args[1], exec_state, invoke_ctx, context)?; - let contract = &env.contract_context.contract_identifier; + let contract = &invoke_ctx.contract_context.contract_identifier; - let data_types = env.contract_context.meta_data_map.get(map_name).ok_or( - RuntimeCheckErrorKind::Unreachable(format!("No such map: {map_name}")), - )?; + let data_types = invoke_ctx + .contract_context + .meta_data_map + .get(map_name) + .ok_or(RuntimeCheckErrorKind::Unreachable(format!( + "No such map: {map_name}" + )))?; runtime_cost( ClarityCostFunction::FetchEntry, - env, + exec_state, data_types.value_type.size()? + data_types.key_type.size()?, )?; - let epoch = *env.epoch(); - env.global_context - .database - .fetch_entry(contract, map_name, &key, data_types, &epoch) + let epoch = *exec_state.epoch(); + exec_state.global_context.database.fetch_entry( + contract, + map_name, + key.as_ref(), + data_types, + &epoch, + ) } /// The Stacks v205 version of fetch_entry uses the actual stored size of the /// value as input to the cost tabulation. Otherwise identical to v200. pub fn special_fetch_entry_v205( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; @@ -455,69 +525,83 @@ pub fn special_fetch_entry_v205( "Expected name".to_string(), ))?; - let key = eval(&args[1], env, context)?; + let key = eval(&args[1], exec_state, invoke_ctx, context)?; - let contract = &env.contract_context.contract_identifier; + let contract = &invoke_ctx.contract_context.contract_identifier; - let data_types = env.contract_context.meta_data_map.get(map_name).ok_or( - RuntimeCheckErrorKind::Unreachable(format!("No such map: {map_name}")), - )?; + let data_types = invoke_ctx + .contract_context + .meta_data_map + .get(map_name) + .ok_or(RuntimeCheckErrorKind::Unreachable(format!( + "No such map: {map_name}" + )))?; - let epoch = *env.epoch(); - let result = env - .global_context - .database - .fetch_entry_with_size(contract, map_name, &key, data_types, &epoch); + let epoch = *exec_state.epoch(); + let result = exec_state.global_context.database.fetch_entry_with_size( + contract, + map_name, + key.as_ref(), + data_types, + &epoch, + ); let result_size = match &result { Ok(data) => data.serialized_byte_len, Err(_e) => (data_types.value_type.size()? + data_types.key_type.size()?).into(), }; - runtime_cost(ClarityCostFunction::FetchEntry, env, result_size)?; + runtime_cost(ClarityCostFunction::FetchEntry, exec_state, result_size)?; result.map(|data| data.value) } pub fn special_at_block( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { + if !exec_state.epoch().supports_at_block() { + return Err(RuntimeCheckErrorKind::AtBlockUnavailable.into()); + } check_argument_count(2, args)?; - runtime_cost(ClarityCostFunction::AtBlock, env, 0)?; - - let bhh = match eval(&args[0], env, context)? { + runtime_cost(ClarityCostFunction::AtBlock, exec_state, 0)?; + let value = eval(&args[0], exec_state, invoke_ctx, context)?; + let bhh = match value.as_ref() { Value::Sequence(SequenceData::Buffer(BuffData { data })) => { if data.len() != 32 { - return Err(RuntimeError::BadBlockHash(data).into()); + return Err(RuntimeError::BadBlockHash(data.clone()).into()); } else { StacksBlockId::from(data.as_slice()) } } - x => { + _ => { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_32), - Box::new(x), + value.as_ref().to_error_string(), ) .into()); } }; - env.add_memory(cost_constants::AT_BLOCK_MEMORY)?; - let result = env.evaluate_at_block(bhh, &args[1], context); - env.drop_memory(cost_constants::AT_BLOCK_MEMORY)?; + exec_state.add_memory(cost_constants::AT_BLOCK_MEMORY)?; + let result = exec_state + .evaluate_at_block(bhh, &args[1], invoke_ctx, context) + .and_then(|v| v.clone_with_cost(exec_state)); + exec_state.drop_memory(cost_constants::AT_BLOCK_MEMORY)?; result } pub fn special_set_entry_v200( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { - if env.global_context.is_read_only() { + if exec_state.global_context.is_read_only() { return Err( RuntimeCheckErrorKind::Unreachable("Write attempted in read-only".to_string()).into(), ); @@ -525,9 +609,9 @@ pub fn special_set_entry_v200( check_argument_count(3, args)?; - let key = eval(&args[1], env, context)?; + let key = eval(&args[1], exec_state, invoke_ctx, context)?; - let value = eval(&args[2], env, context)?; + let value = eval(&args[2], exec_state, invoke_ctx, context)?; let map_name = args[0] .match_atom() @@ -535,23 +619,30 @@ pub fn special_set_entry_v200( "Expected name".to_string(), ))?; - let contract = &env.contract_context.contract_identifier; + let contract = &invoke_ctx.contract_context.contract_identifier; - let data_types = env.contract_context.meta_data_map.get(map_name).ok_or( - RuntimeCheckErrorKind::Unreachable(format!("No such map: {map_name}")), - )?; + let data_types = invoke_ctx + .contract_context + .meta_data_map + .get(map_name) + .ok_or(RuntimeCheckErrorKind::Unreachable(format!( + "No such map: {map_name}" + )))?; runtime_cost( ClarityCostFunction::SetEntry, - env, + exec_state, data_types.value_type.size()? + data_types.key_type.size()?, )?; - env.add_memory(key.get_memory_use()?)?; - env.add_memory(value.get_memory_use()?)?; + exec_state.add_memory(key.as_ref().get_memory_use()?)?; + exec_state.add_memory(value.as_ref().get_memory_use()?)?; - let epoch = *env.epoch(); - env.global_context + let key = key.clone_with_cost(exec_state)?; + let value = value.clone_with_cost(exec_state)?; + let epoch = *exec_state.epoch(); + exec_state + .global_context .database .set_entry(contract, map_name, key, value, data_types, &epoch) .map(|data| data.value) @@ -561,10 +652,11 @@ pub fn special_set_entry_v200( /// value as input to the cost tabulation. Otherwise identical to v200. pub fn special_set_entry_v205( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { - if env.global_context.is_read_only() { + if exec_state.global_context.is_read_only() { return Err( RuntimeCheckErrorKind::Unreachable("Write attempted in read-only".to_string()).into(), ); @@ -572,9 +664,9 @@ pub fn special_set_entry_v205( check_argument_count(3, args)?; - let key = eval(&args[1], env, context)?; + let key = eval(&args[1], exec_state, invoke_ctx, context)?; - let value = eval(&args[2], env, context)?; + let value = eval(&args[2], exec_state, invoke_ctx, context)?; let map_name = args[0] .match_atom() @@ -582,14 +674,20 @@ pub fn special_set_entry_v205( "Expected name".to_string(), ))?; - let contract = &env.contract_context.contract_identifier; + let contract = &invoke_ctx.contract_context.contract_identifier; - let data_types = env.contract_context.meta_data_map.get(map_name).ok_or( - RuntimeCheckErrorKind::Unreachable(format!("No such map: {map_name}")), - )?; + let data_types = invoke_ctx + .contract_context + .meta_data_map + .get(map_name) + .ok_or(RuntimeCheckErrorKind::Unreachable(format!( + "No such map: {map_name}" + )))?; - let epoch = *env.epoch(); - let result = env + let key = key.clone_with_cost(exec_state)?; + let value = value.clone_with_cost(exec_state)?; + let epoch = *exec_state.epoch(); + let result = exec_state .global_context .database .set_entry(contract, map_name, key, value, data_types, &epoch); @@ -599,19 +697,20 @@ pub fn special_set_entry_v205( Err(_e) => (data_types.value_type.size()? + data_types.key_type.size()?).into(), }; - runtime_cost(ClarityCostFunction::SetEntry, env, result_size)?; + runtime_cost(ClarityCostFunction::SetEntry, exec_state, result_size)?; - env.add_memory(result_size)?; + exec_state.add_memory(result_size)?; result.map(|data| data.value) } pub fn special_insert_entry_v200( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { - if env.global_context.is_read_only() { + if exec_state.global_context.is_read_only() { return Err( RuntimeCheckErrorKind::Unreachable("Write attempted in read-only".to_string()).into(), ); @@ -619,9 +718,9 @@ pub fn special_insert_entry_v200( check_argument_count(3, args)?; - let key = eval(&args[1], env, context)?; + let key = eval(&args[1], exec_state, invoke_ctx, context)?; - let value = eval(&args[2], env, context)?; + let value = eval(&args[2], exec_state, invoke_ctx, context)?; let map_name = args[0] .match_atom() @@ -629,24 +728,31 @@ pub fn special_insert_entry_v200( "Expected name".to_string(), ))?; - let contract = &env.contract_context.contract_identifier; + let contract = &invoke_ctx.contract_context.contract_identifier; - let data_types = env.contract_context.meta_data_map.get(map_name).ok_or( - RuntimeCheckErrorKind::Unreachable(format!("No such map: {map_name}")), - )?; + let data_types = invoke_ctx + .contract_context + .meta_data_map + .get(map_name) + .ok_or(RuntimeCheckErrorKind::Unreachable(format!( + "No such map: {map_name}" + )))?; runtime_cost( ClarityCostFunction::SetEntry, - env, + exec_state, data_types.value_type.size()? + data_types.key_type.size()?, )?; - env.add_memory(key.get_memory_use()?)?; - env.add_memory(value.get_memory_use()?)?; + exec_state.add_memory(key.as_ref().get_memory_use()?)?; + exec_state.add_memory(value.as_ref().get_memory_use()?)?; - let epoch = *env.epoch(); + let epoch = *exec_state.epoch(); - env.global_context + let key = key.clone_with_cost(exec_state)?; + let value = value.clone_with_cost(exec_state)?; + exec_state + .global_context .database .insert_entry(contract, map_name, key, value, data_types, &epoch) .map(|data| data.value) @@ -656,10 +762,11 @@ pub fn special_insert_entry_v200( /// value as input to the cost tabulation. Otherwise identical to v200. pub fn special_insert_entry_v205( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { - if env.global_context.is_read_only() { + if exec_state.global_context.is_read_only() { return Err( RuntimeCheckErrorKind::Unreachable("Write attempted in read-only".to_string()).into(), ); @@ -667,9 +774,9 @@ pub fn special_insert_entry_v205( check_argument_count(3, args)?; - let key = eval(&args[1], env, context)?; + let key = eval(&args[1], exec_state, invoke_ctx, context)?; - let value = eval(&args[2], env, context)?; + let value = eval(&args[2], exec_state, invoke_ctx, context)?; let map_name = args[0] .match_atom() @@ -677,14 +784,20 @@ pub fn special_insert_entry_v205( "Expected name".to_string(), ))?; - let contract = &env.contract_context.contract_identifier; + let contract = &invoke_ctx.contract_context.contract_identifier; - let data_types = env.contract_context.meta_data_map.get(map_name).ok_or( - RuntimeCheckErrorKind::Unreachable(format!("No such map: {map_name}")), - )?; + let data_types = invoke_ctx + .contract_context + .meta_data_map + .get(map_name) + .ok_or(RuntimeCheckErrorKind::Unreachable(format!( + "No such map: {map_name}" + )))?; - let epoch = *env.epoch(); - let result = env + let key = key.clone_with_cost(exec_state)?; + let value = value.clone_with_cost(exec_state)?; + let epoch = *exec_state.epoch(); + let result = exec_state .global_context .database .insert_entry(contract, map_name, key, value, data_types, &epoch); @@ -694,19 +807,20 @@ pub fn special_insert_entry_v205( Err(_e) => (data_types.value_type.size()? + data_types.key_type.size()?).into(), }; - runtime_cost(ClarityCostFunction::SetEntry, env, result_size)?; + runtime_cost(ClarityCostFunction::SetEntry, exec_state, result_size)?; - env.add_memory(result_size)?; + exec_state.add_memory(result_size)?; result.map(|data| data.value) } pub fn special_delete_entry_v200( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { - if env.global_context.is_read_only() { + if exec_state.global_context.is_read_only() { return Err( RuntimeCheckErrorKind::Unreachable("Write attempted in read-only".to_string()).into(), ); @@ -714,7 +828,7 @@ pub fn special_delete_entry_v200( check_argument_count(2, args)?; - let key = eval(&args[1], env, context)?; + let key = eval(&args[1], exec_state, invoke_ctx, context)?; let map_name = args[0] .match_atom() @@ -722,24 +836,29 @@ pub fn special_delete_entry_v200( "Expected name".to_string(), ))?; - let contract = &env.contract_context.contract_identifier; + let contract = &invoke_ctx.contract_context.contract_identifier; - let data_types = env.contract_context.meta_data_map.get(map_name).ok_or( - RuntimeCheckErrorKind::Unreachable(format!("No such map: {map_name}")), - )?; + let data_types = invoke_ctx + .contract_context + .meta_data_map + .get(map_name) + .ok_or(RuntimeCheckErrorKind::Unreachable(format!( + "No such map: {map_name}" + )))?; runtime_cost( ClarityCostFunction::SetEntry, - env, + exec_state, data_types.key_type.size()?, )?; - env.add_memory(key.get_memory_use()?)?; + exec_state.add_memory(key.as_ref().get_memory_use()?)?; - let epoch = *env.epoch(); - env.global_context + let epoch = *exec_state.epoch(); + exec_state + .global_context .database - .delete_entry(contract, map_name, &key, data_types, &epoch) + .delete_entry(contract, map_name, key.as_ref(), data_types, &epoch) .map(|data| data.value) } @@ -747,10 +866,11 @@ pub fn special_delete_entry_v200( /// value as input to the cost tabulation. Otherwise identical to v200. pub fn special_delete_entry_v205( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { - if env.global_context.is_read_only() { + if exec_state.global_context.is_read_only() { return Err( RuntimeCheckErrorKind::Unreachable("Write attempted in read-only".to_string()).into(), ); @@ -758,7 +878,7 @@ pub fn special_delete_entry_v205( check_argument_count(2, args)?; - let key = eval(&args[1], env, context)?; + let key = eval(&args[1], exec_state, invoke_ctx, context)?; let map_name = args[0] .match_atom() @@ -766,26 +886,33 @@ pub fn special_delete_entry_v205( "Expected name".to_string(), ))?; - let contract = &env.contract_context.contract_identifier; - - let data_types = env.contract_context.meta_data_map.get(map_name).ok_or( - RuntimeCheckErrorKind::Unreachable(format!("No such map: {map_name}")), - )?; - - let epoch = *env.epoch(); - let result = env - .global_context - .database - .delete_entry(contract, map_name, &key, data_types, &epoch); + let contract = &invoke_ctx.contract_context.contract_identifier; + + let data_types = invoke_ctx + .contract_context + .meta_data_map + .get(map_name) + .ok_or(RuntimeCheckErrorKind::Unreachable(format!( + "No such map: {map_name}" + )))?; + + let epoch = *exec_state.epoch(); + let result = exec_state.global_context.database.delete_entry( + contract, + map_name, + key.as_ref(), + data_types, + &epoch, + ); let result_size = match &result { Ok(data) => data.serialized_byte_len, Err(_e) => data_types.key_type.size()?.into(), }; - runtime_cost(ClarityCostFunction::SetEntry, env, result_size)?; + runtime_cost(ClarityCostFunction::SetEntry, exec_state, result_size)?; - env.add_memory(result_size)?; + exec_state.add_memory(result_size)?; result.map(|data| data.value) } @@ -812,11 +939,12 @@ pub fn special_delete_entry_v205( /// - [`VmInternalError`] from database operations when retrieving block information. pub fn special_get_block_info( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { // (get-block-info? property-name block-height-uint) - runtime_cost(ClarityCostFunction::BlockInfo, env, 0)?; + runtime_cost(ClarityCostFunction::BlockInfo, exec_state, 0)?; check_argument_count(2, args)?; @@ -827,7 +955,7 @@ pub fn special_get_block_info( "Get block info expect property name".to_string(), ))?; - let version = env.contract_context.get_clarity_version(); + let version = invoke_ctx.contract_context.get_clarity_version(); let block_info_prop = BlockInfoProperty::lookup_by_name_at_version(property_name, version) .ok_or(RuntimeCheckErrorKind::Unreachable( @@ -835,12 +963,12 @@ pub fn special_get_block_info( ))?; // Handle the block-height input arg clause. - let height_eval = eval(&args[1], env, context)?; - let height_value = match height_eval { - Value::UInt(result) => Ok(result), - x => Err(RuntimeCheckErrorKind::TypeValueError( + let height_eval = eval(&args[1], exec_state, invoke_ctx, context)?; + let height_value = match height_eval.as_ref() { + Value::UInt(result) => Ok(*result), + _ => Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::UIntType), - Box::new(x), + height_eval.as_ref().to_error_string(), )), }?; @@ -853,16 +981,16 @@ pub fn special_get_block_info( // * clarity version is less than Clarity3 // * the evaluated epoch is geq 3.0 // * we are not on (classic) primary testnet - let interpret_height_as_tenure_height = env.contract_context.get_clarity_version() + let interpret_height_as_tenure_height = invoke_ctx.contract_context.get_clarity_version() < &ClarityVersion::Clarity3 - && env.global_context.epoch_id >= StacksEpochId::Epoch30 - && env.global_context.chain_id != CHAIN_ID_TESTNET; + && exec_state.global_context.epoch_id >= StacksEpochId::Epoch30 + && exec_state.global_context.chain_id != CHAIN_ID_TESTNET; let height_value = if !interpret_height_as_tenure_height { height_value } else { // interpretting height_value as a tenure height - let height_opt = env + let height_opt = exec_state .global_context .database .get_block_height_for_tenure_height(height_value)?; @@ -872,21 +1000,24 @@ pub fn special_get_block_info( } }; - let current_block_height = env.global_context.database.get_current_block_height(); + let current_block_height = exec_state + .global_context + .database + .get_current_block_height(); if height_value >= current_block_height { return Ok(Value::none()); } let result = match block_info_prop { BlockInfoProperty::Time => { - let block_time = env + let block_time = exec_state .global_context .database .get_burn_block_time(height_value, None)?; Value::UInt(u128::from(block_time)) } BlockInfoProperty::VrfSeed => { - let vrf_seed = env + let vrf_seed = exec_state .global_context .database .get_block_vrf_seed(height_value)?; @@ -895,7 +1026,7 @@ pub fn special_get_block_info( })) } BlockInfoProperty::HeaderHash => { - let header_hash = env + let header_hash = exec_state .global_context .database .get_block_header_hash(height_value)?; @@ -904,7 +1035,7 @@ pub fn special_get_block_info( })) } BlockInfoProperty::BurnchainHeaderHash => { - let burnchain_header_hash = env + let burnchain_header_hash = exec_state .global_context .database .get_burnchain_block_header_hash(height_value)?; @@ -913,7 +1044,7 @@ pub fn special_get_block_info( })) } BlockInfoProperty::IdentityHeaderHash => { - let id_header_hash = env + let id_header_hash = exec_state .global_context .database .get_index_block_header_hash(height_value)?; @@ -922,21 +1053,21 @@ pub fn special_get_block_info( })) } BlockInfoProperty::MinerAddress => { - let miner_address = env + let miner_address = exec_state .global_context .database .get_miner_address(height_value)?; Value::from(miner_address) } BlockInfoProperty::MinerSpendWinner => { - let winner_spend = env + let winner_spend = exec_state .global_context .database .get_miner_spend_winner(height_value)?; Value::UInt(winner_spend) } BlockInfoProperty::MinerSpendTotal => { - let total_spend = env + let total_spend = exec_state .global_context .database .get_miner_spend_total(height_value)?; @@ -944,7 +1075,10 @@ pub fn special_get_block_info( } BlockInfoProperty::BlockReward => { // this is already an optional - let block_reward_opt = env.global_context.database.get_block_reward(height_value)?; + let block_reward_opt = exec_state + .global_context + .database + .get_block_reward(height_value)?; return Ok(match block_reward_opt { Some(x) => Value::some(Value::UInt(x))?, None => Value::none(), @@ -971,10 +1105,11 @@ pub fn special_get_block_info( /// - [`VmInternalError`] from database operations or value construction failures. pub fn special_get_burn_block_info( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { - runtime_cost(ClarityCostFunction::GetBurnBlockInfo, env, 0)?; + runtime_cost(ClarityCostFunction::GetBurnBlockInfo, exec_state, 0)?; check_argument_count(2, args)?; @@ -992,13 +1127,13 @@ pub fn special_get_burn_block_info( )?; // Handle the block-height input arg clause. - let height_eval = eval(&args[1], env, context)?; - let height_value = match height_eval { - Value::UInt(result) => result, - x => { + let height_eval = eval(&args[1], exec_state, invoke_ctx, context)?; + let height_value = match height_eval.as_ref() { + Value::UInt(result) => *result, + _ => { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::UIntType), - Box::new(x), + height_eval.as_ref().to_error_string(), ) .into()); } @@ -1012,7 +1147,7 @@ pub fn special_get_burn_block_info( match block_info_prop { BurnBlockInfoProperty::HeaderHash => { - let burnchain_header_hash_opt = env + let burnchain_header_hash_opt = exec_state .global_context .database .get_burnchain_block_header_hash_for_burnchain_height(height_value)?; @@ -1027,7 +1162,7 @@ pub fn special_get_burn_block_info( } } BurnBlockInfoProperty::PoxAddrs => { - let pox_addrs_and_payout = env + let pox_addrs_and_payout = exec_state .global_context .database .get_pox_payout_addrs_for_burnchain_height(height_value)?; @@ -1039,7 +1174,7 @@ pub fn special_get_burn_block_info( "addrs".into(), Value::cons_list( addrs.into_iter().map(Value::Tuple).collect(), - env.epoch(), + exec_state.epoch(), ) .map_err(|_| { VmInternalError::Expect( @@ -1078,11 +1213,12 @@ pub fn special_get_burn_block_info( /// - [`VmInternalError`] from database operations when retrieving block information. pub fn special_get_stacks_block_info( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { // (get-stacks-block-info? property-name block-height-uint) - runtime_cost(ClarityCostFunction::BlockInfo, env, 0)?; + runtime_cost(ClarityCostFunction::BlockInfo, exec_state, 0)?; check_argument_count(2, args)?; @@ -1100,12 +1236,12 @@ pub fn special_get_stacks_block_info( )?; // Handle the block-height input arg. - let height_eval = eval(&args[1], env, context)?; - let height_value = match height_eval { - Value::UInt(result) => Ok(result), - x => Err(RuntimeCheckErrorKind::TypeValueError( + let height_eval = eval(&args[1], exec_state, invoke_ctx, context)?; + let height_value = match height_eval.as_ref() { + Value::UInt(result) => Ok(*result), + _ => Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::UIntType), - Box::new(x), + height_eval.as_ref().to_error_string(), )), }?; @@ -1113,18 +1249,24 @@ pub fn special_get_stacks_block_info( return Ok(Value::none()); }; - let current_block_height = env.global_context.database.get_current_block_height(); + let current_block_height = exec_state + .global_context + .database + .get_current_block_height(); if height_value >= current_block_height { return Ok(Value::none()); } let result = match block_info_prop { StacksBlockInfoProperty::Time => { - let block_time = env.global_context.database.get_block_time(height_value)?; + let block_time = exec_state + .global_context + .database + .get_block_time(height_value)?; Value::UInt(u128::from(block_time)) } StacksBlockInfoProperty::HeaderHash => { - let header_hash = env + let header_hash = exec_state .global_context .database .get_block_header_hash(height_value)?; @@ -1133,7 +1275,7 @@ pub fn special_get_stacks_block_info( })) } StacksBlockInfoProperty::IndexHeaderHash => { - let id_header_hash = env + let id_header_hash = exec_state .global_context .database .get_index_block_header_hash(height_value)?; @@ -1166,11 +1308,12 @@ pub fn special_get_stacks_block_info( /// - [`VmInternalError`] from database operations when retrieving tenure information. pub fn special_get_tenure_info( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { // (get-tenure-info? property-name block-height-uint) - runtime_cost(ClarityCostFunction::BlockInfo, env, 0)?; + runtime_cost(ClarityCostFunction::BlockInfo, exec_state, 0)?; check_argument_count(2, args)?; @@ -1186,12 +1329,12 @@ pub fn special_get_tenure_info( )?; // Handle the block-height input arg. - let height_eval = eval(&args[1], env, context)?; - let height_value = match height_eval { - Value::UInt(result) => Ok(result), - x => Err(RuntimeCheckErrorKind::TypeValueError( + let height_eval = eval(&args[1], exec_state, invoke_ctx, context)?; + let height_value = match height_eval.as_ref() { + Value::UInt(result) => Ok(*result), + _ => Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::UIntType), - Box::new(x), + height_eval.as_ref().to_error_string(), )), }?; @@ -1199,21 +1342,24 @@ pub fn special_get_tenure_info( return Ok(Value::none()); }; - let current_height = env.global_context.database.get_current_block_height(); + let current_height = exec_state + .global_context + .database + .get_current_block_height(); if height_value >= current_height { return Ok(Value::none()); } let result = match block_info_prop { TenureInfoProperty::Time => { - let block_time = env + let block_time = exec_state .global_context .database .get_burn_block_time(height_value, None)?; Value::UInt(u128::from(block_time)) } TenureInfoProperty::VrfSeed => { - let vrf_seed = env + let vrf_seed = exec_state .global_context .database .get_block_vrf_seed(height_value)?; @@ -1222,7 +1368,7 @@ pub fn special_get_tenure_info( })) } TenureInfoProperty::BurnchainHeaderHash => { - let burnchain_header_hash = env + let burnchain_header_hash = exec_state .global_context .database .get_burnchain_block_header_hash(height_value)?; @@ -1231,21 +1377,21 @@ pub fn special_get_tenure_info( })) } TenureInfoProperty::MinerAddress => { - let miner_address = env + let miner_address = exec_state .global_context .database .get_miner_address(height_value)?; Value::from(miner_address) } TenureInfoProperty::MinerSpendWinner => { - let winner_spend = env + let winner_spend = exec_state .global_context .database .get_miner_spend_winner(height_value)?; Value::UInt(winner_spend) } TenureInfoProperty::MinerSpendTotal => { - let total_spend = env + let total_spend = exec_state .global_context .database .get_miner_spend_total(height_value)?; @@ -1253,7 +1399,10 @@ pub fn special_get_tenure_info( } TenureInfoProperty::BlockReward => { // this is already an optional - let block_reward_opt = env.global_context.database.get_block_reward(height_value)?; + let block_reward_opt = exec_state + .global_context + .database + .get_block_reward(height_value)?; return Ok(match block_reward_opt { Some(x) => Value::some(Value::UInt(x))?, None => Value::none(), @@ -1267,15 +1416,16 @@ pub fn special_get_tenure_info( /// Handles the function `contract-hash?` pub fn special_contract_hash( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(1, args)?; let contract_expr = args .first() .ok_or(RuntimeCheckErrorKind::IncorrectArgumentCount(1, 0))?; - let contract_value = eval(contract_expr, env, context)?; - let contract_identifier = match contract_value { + let contract_value = eval(contract_expr, exec_state, invoke_ctx, context)?; + let contract_identifier = match contract_value.as_ref() { Value::Principal(PrincipalData::Standard(_)) => { // If the value is a standard principal, we return `(err u1)`. return Ok(Value::err_uint(1)); @@ -1283,19 +1433,19 @@ pub fn special_contract_hash( Value::Principal(PrincipalData::Contract(contract_identifier)) => contract_identifier, _ => { // If the value is not a principal, we return a RuntimeCheckErrorKind. - return Err( - RuntimeCheckErrorKind::ExpectedContractPrincipalValue(Box::new(contract_value)) - .into(), - ); + return Err(RuntimeCheckErrorKind::ExpectedContractPrincipalValue( + contract_value.as_ref().to_error_string(), + ) + .into()); } }; - runtime_cost(ClarityCostFunction::ContractHash, env, 0)?; + runtime_cost(ClarityCostFunction::ContractHash, exec_state, 0)?; - let Some(contract_hash) = env + let Some(contract_hash) = exec_state .global_context .database - .get_contract_hash(&contract_identifier)? + .get_contract_hash(contract_identifier)? else { // If the contract does not exist, we return `(err u2)`. return Ok(Value::err_uint(2)); diff --git a/clarity/src/vm/functions/define.rs b/clarity/src/vm/functions/define.rs index e692ef9e9e3..49d0df8956c 100644 --- a/clarity/src/vm/functions/define.rs +++ b/clarity/src/vm/functions/define.rs @@ -17,7 +17,7 @@ use std::collections::BTreeMap; use crate::vm::callables::{DefineType, DefinedFunction}; -use crate::vm::contexts::{ContractContext, Environment, LocalContext}; +use crate::vm::contexts::{ContractContext, ExecutionState, InvocationContext, LocalContext}; use crate::vm::errors::{ CommonCheckErrorKind, RuntimeCheckErrorKind, SyntaxBindingErrorType, VmExecutionError, check_argument_count, check_arguments_at_least, @@ -27,8 +27,7 @@ use crate::vm::representations::SymbolicExpressionType::Field; use crate::vm::representations::{ClarityName, SymbolicExpression}; use crate::vm::types::signatures::FunctionSignature; use crate::vm::types::{ - CallableData, PrincipalData, TraitIdentifier, TypeSignature, TypeSignatureExt as _, Value, - parse_name_type_pairs, + TraitIdentifier, TypeSignature, TypeSignatureExt as _, Value, parse_name_type_pairs, }; define_named_enum!(DefineFunctions { @@ -135,37 +134,21 @@ fn check_legal_define( fn handle_define_variable( variable: &ClarityName, expression: &SymbolicExpression, - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, ) -> Result { // is the variable name legal? - check_legal_define(variable, env.contract_context)?; + check_legal_define(variable, invoke_ctx.contract_context)?; let context = LocalContext::new(); - let raw_value = eval(expression, env, &context)?; - let value = if env - .contract_context - .get_clarity_version() - .supports_callables() - && env.epoch().supports_call_with_constant() - { - match raw_value { - Value::Principal(PrincipalData::Contract(contract_identifier)) => { - Value::CallableContract(CallableData { - contract_identifier, - trait_identifier: None, - }) - } - v => v, - } - } else { - raw_value - }; + let value = eval(expression, exec_state, invoke_ctx, &context)?.clone_with_cost(exec_state)?; Ok(DefineResult::Variable(variable.clone(), value)) } fn handle_define_function( signature: &[SymbolicExpression], expression: &SymbolicExpression, - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, define_type: DefineType, ) -> Result { let (function_symbol, arg_symbols) = @@ -181,17 +164,17 @@ fn handle_define_function( "Expected name".to_string(), ))?; - check_legal_define(function_name, env.contract_context)?; + check_legal_define(function_name, invoke_ctx.contract_context)?; let arguments = parse_name_type_pairs::<_, RuntimeCheckErrorKind>( - *env.epoch(), + *exec_state.epoch(), arg_symbols, SyntaxBindingErrorType::Eval, - env, + exec_state, )?; for (argument, _) in arguments.iter() { - check_legal_define(argument, env.contract_context)?; + check_legal_define(argument, invoke_ctx.contract_context)?; } let function = DefinedFunction::new( @@ -199,7 +182,7 @@ fn handle_define_function( expression.clone(), define_type, function_name, - &env.contract_context.contract_identifier.to_string(), + &invoke_ctx.contract_context.contract_identifier.to_string(), ); Ok(DefineResult::Function(function_name.clone(), function)) @@ -209,14 +192,16 @@ fn handle_define_persisted_variable( variable_str: &ClarityName, value_type: &SymbolicExpression, value: &SymbolicExpression, - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, ) -> Result { - check_legal_define(variable_str, env.contract_context)?; + check_legal_define(variable_str, invoke_ctx.contract_context)?; - let value_type_signature = TypeSignature::parse_type_repr(*env.epoch(), value_type, env)?; + let value_type_signature = + TypeSignature::parse_type_repr(*exec_state.epoch(), value_type, exec_state)?; let context = LocalContext::new(); - let value = eval(value, env, &context)?; + let value = eval(value, exec_state, invoke_ctx, &context)?.clone_with_cost(exec_state)?; Ok(DefineResult::PersistedVariable( variable_str.clone(), @@ -228,11 +213,13 @@ fn handle_define_persisted_variable( fn handle_define_nonfungible_asset( asset_name: &ClarityName, key_type: &SymbolicExpression, - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, ) -> Result { - check_legal_define(asset_name, env.contract_context)?; + check_legal_define(asset_name, invoke_ctx.contract_context)?; - let key_type_signature = TypeSignature::parse_type_repr(*env.epoch(), key_type, env)?; + let key_type_signature = + TypeSignature::parse_type_repr(*exec_state.epoch(), key_type, exec_state)?; Ok(DefineResult::NonFungibleAsset( asset_name.clone(), @@ -243,22 +230,23 @@ fn handle_define_nonfungible_asset( fn handle_define_fungible_token( asset_name: &ClarityName, total_supply: Option<&SymbolicExpression>, - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, ) -> Result { - check_legal_define(asset_name, env.contract_context)?; + check_legal_define(asset_name, invoke_ctx.contract_context)?; if let Some(total_supply_expr) = total_supply { let context = LocalContext::new(); - let total_supply_value = eval(total_supply_expr, env, &context)?; - if let Value::UInt(total_supply_int) = total_supply_value { + let total_supply_value = eval(total_supply_expr, exec_state, invoke_ctx, &context)?; + if let Value::UInt(total_supply_int) = total_supply_value.as_ref() { Ok(DefineResult::FungibleToken( asset_name.clone(), - Some(total_supply_int), + Some(*total_supply_int), )) } else { Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::UIntType), - Box::new(total_supply_value), + total_supply_value.as_ref().to_error_string(), ) .into()) } @@ -271,12 +259,15 @@ fn handle_define_map( map_str: &ClarityName, key_type: &SymbolicExpression, value_type: &SymbolicExpression, - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, ) -> Result { - check_legal_define(map_str, env.contract_context)?; + check_legal_define(map_str, invoke_ctx.contract_context)?; - let key_type_signature = TypeSignature::parse_type_repr(*env.epoch(), key_type, env)?; - let value_type_signature = TypeSignature::parse_type_repr(*env.epoch(), value_type, env)?; + let key_type_signature = + TypeSignature::parse_type_repr(*exec_state.epoch(), key_type, exec_state)?; + let value_type_signature = + TypeSignature::parse_type_repr(*exec_state.epoch(), value_type, exec_state)?; Ok(DefineResult::Map( map_str.clone(), @@ -288,15 +279,16 @@ fn handle_define_map( fn handle_define_trait( name: &ClarityName, functions: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, ) -> Result { - check_legal_define(name, env.contract_context)?; + check_legal_define(name, invoke_ctx.contract_context)?; let trait_signature = TypeSignature::parse_trait_type_repr( functions, - env, - *env.epoch(), - *env.contract_context.get_clarity_version(), + exec_state, + *exec_state.epoch(), + *invoke_ctx.contract_context.get_clarity_version(), )?; Ok(DefineResult::Trait(name.clone(), trait_signature)) @@ -460,43 +452,48 @@ impl<'a> DefineFunctionsParsed<'a> { pub fn evaluate_define( expression: &SymbolicExpression, - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, ) -> Result { if let Some(define_type) = DefineFunctionsParsed::try_parse(expression)? { match define_type { DefineFunctionsParsed::Constant { name, value } => { - handle_define_variable(name, value, env) + handle_define_variable(name, value, exec_state, invoke_ctx) } DefineFunctionsParsed::PrivateFunction { signature, body } => { - handle_define_function(signature, body, env, DefineType::Private) - } - DefineFunctionsParsed::ReadOnlyFunction { signature, body } => { - handle_define_function(signature, body, env, DefineType::ReadOnly) + handle_define_function(signature, body, exec_state, invoke_ctx, DefineType::Private) } + DefineFunctionsParsed::ReadOnlyFunction { signature, body } => handle_define_function( + signature, + body, + exec_state, + invoke_ctx, + DefineType::ReadOnly, + ), DefineFunctionsParsed::PublicFunction { signature, body } => { - handle_define_function(signature, body, env, DefineType::Public) + handle_define_function(signature, body, exec_state, invoke_ctx, DefineType::Public) } DefineFunctionsParsed::NonFungibleToken { name, nft_type } => { - handle_define_nonfungible_asset(name, nft_type, env) + handle_define_nonfungible_asset(name, nft_type, exec_state, invoke_ctx) } DefineFunctionsParsed::BoundedFungibleToken { name, max_supply } => { - handle_define_fungible_token(name, Some(max_supply), env) + handle_define_fungible_token(name, Some(max_supply), exec_state, invoke_ctx) } DefineFunctionsParsed::UnboundedFungibleToken { name } => { - handle_define_fungible_token(name, None, env) + handle_define_fungible_token(name, None, exec_state, invoke_ctx) } DefineFunctionsParsed::Map { name, key_type, value_type, - } => handle_define_map(name, key_type, value_type, env), + } => handle_define_map(name, key_type, value_type, exec_state, invoke_ctx), DefineFunctionsParsed::PersistedVariable { name, data_type, initial, - } => handle_define_persisted_variable(name, data_type, initial, env), + } => handle_define_persisted_variable(name, data_type, initial, exec_state, invoke_ctx), DefineFunctionsParsed::Trait { name, functions } => { - handle_define_trait(name, functions, env) + handle_define_trait(name, functions, exec_state, invoke_ctx) } DefineFunctionsParsed::UseTrait { name, @@ -522,13 +519,13 @@ mod test { use crate::vm::analysis::type_checker::v2_1::MAX_FUNCTION_PARAMETERS; use crate::vm::callables::DefineType; - use crate::vm::contexts::GlobalContext; + use crate::vm::contexts::{ExecutionState, GlobalContext, InvocationContext}; use crate::vm::costs::LimitedCostTracker; use crate::vm::database::MemoryBackingStore; use crate::vm::errors::VmExecutionError; use crate::vm::functions::define::{handle_define_function, handle_define_trait}; use crate::vm::tests::test_clarity_versions; - use crate::vm::{CallStack, ClarityVersion, ContractContext, Environment, LocalContext}; + use crate::vm::{CallStack, ClarityVersion, ContractContext, LocalContext}; #[apply(test_clarity_versions)] fn bad_syntax_binding_define_function( @@ -559,17 +556,25 @@ mod test { let context = LocalContext::new(); let mut call_stack = CallStack::new(); - let mut env = Environment::new( - &mut global_context, - &contract_context, - &mut call_stack, - None, - None, - None, - ); + let mut exec_state = ExecutionState { + global_context: &mut global_context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { + contract_context: &contract_context, + sender: None, + caller: None, + sponsor: None, + }; - let err = handle_define_function(&bad_signature, &body, &mut env, DefineType::Public) - .unwrap_err(); + let err = handle_define_function( + &bad_signature, + &body, + &mut exec_state, + &invoke_ctx, + DefineType::Public, + ) + .unwrap_err(); assert_eq!( VmExecutionError::RuntimeCheck(RuntimeCheckErrorKind::Unreachable( @@ -619,16 +624,24 @@ mod test { let mut call_stack = CallStack::new(); - let mut env = Environment::new( - &mut global_context, - &contract_context, - &mut call_stack, - None, - None, - None, - ); + let mut exec_state = ExecutionState { + global_context: &mut global_context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { + contract_context: &contract_context, + sender: None, + caller: None, + sponsor: None, + }; - let err = handle_define_trait(&"bad-trait".into(), &trait_body, &mut env).unwrap_err(); + let err = handle_define_trait( + &"bad-trait".into(), + &trait_body, + &mut exec_state, + &invoke_ctx, + ) + .unwrap_err(); assert_eq!( VmExecutionError::RuntimeCheck(RuntimeCheckErrorKind::Unreachable( diff --git a/clarity/src/vm/functions/mod.rs b/clarity/src/vm/functions/mod.rs index 62d2c71e8f9..96ed6a5e07c 100644 --- a/clarity/src/vm/functions/mod.rs +++ b/clarity/src/vm/functions/mod.rs @@ -19,6 +19,7 @@ use stacks_common::types::StacksEpochId; use crate::vm::Value::CallableContract; use crate::vm::callables::{CallableType, NativeHandle, cost_input_sized_vararg}; +use crate::vm::contexts::{ExecutionState, InvocationContext}; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::{CostTracker, MemoryConsumer, constants as cost_constants, runtime_cost}; use crate::vm::errors::{ @@ -28,41 +29,42 @@ use crate::vm::errors::{ pub use crate::vm::functions::assets::stx_transfer_consolidated; use crate::vm::representations::{ClarityName, SymbolicExpression, SymbolicExpressionType}; use crate::vm::types::{PrincipalData, TypeSignature, Value}; -use crate::vm::{Environment, LocalContext, eval, is_reserved}; +use crate::vm::{LocalContext, eval, is_reserved}; macro_rules! switch_on_global_epoch { ($Name:ident ($Epoch2Version:ident, $Epoch205Version:ident)) => { pub fn $Name( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut crate::vm::ExecutionState, + invoke_ctx: &crate::vm::InvocationContext, context: &LocalContext, ) -> std::result::Result { - match env.epoch() { + match exec_state.epoch() { StacksEpochId::Epoch10 => { panic!("Executing Clarity method during Epoch 1.0, before Clarity") } - StacksEpochId::Epoch20 => $Epoch2Version(args, env, context), - StacksEpochId::Epoch2_05 => $Epoch205Version(args, env, context), + StacksEpochId::Epoch20 => $Epoch2Version(args, exec_state, invoke_ctx, context), + StacksEpochId::Epoch2_05 => $Epoch205Version(args, exec_state, invoke_ctx, context), // Note: We reuse 2.05 for 2.1. - StacksEpochId::Epoch21 => $Epoch205Version(args, env, context), + StacksEpochId::Epoch21 => $Epoch205Version(args, exec_state, invoke_ctx, context), // Note: We reuse 2.05 for 2.2. - StacksEpochId::Epoch22 => $Epoch205Version(args, env, context), + StacksEpochId::Epoch22 => $Epoch205Version(args, exec_state, invoke_ctx, context), // Note: We reuse 2.05 for 2.3. - StacksEpochId::Epoch23 => $Epoch205Version(args, env, context), + StacksEpochId::Epoch23 => $Epoch205Version(args, exec_state, invoke_ctx, context), // Note: We reuse 2.05 for 2.4. - StacksEpochId::Epoch24 => $Epoch205Version(args, env, context), + StacksEpochId::Epoch24 => $Epoch205Version(args, exec_state, invoke_ctx, context), // Note: We reuse 2.05 for 2.5. - StacksEpochId::Epoch25 => $Epoch205Version(args, env, context), + StacksEpochId::Epoch25 => $Epoch205Version(args, exec_state, invoke_ctx, context), // Note: We reuse 2.05 for 3.0. - StacksEpochId::Epoch30 => $Epoch205Version(args, env, context), + StacksEpochId::Epoch30 => $Epoch205Version(args, exec_state, invoke_ctx, context), // Note: We reuse 2.05 for 3.1. - StacksEpochId::Epoch31 => $Epoch205Version(args, env, context), + StacksEpochId::Epoch31 => $Epoch205Version(args, exec_state, invoke_ctx, context), // Note: We reuse 2.05 for 3.2. - StacksEpochId::Epoch32 => $Epoch205Version(args, env, context), + StacksEpochId::Epoch32 => $Epoch205Version(args, exec_state, invoke_ctx, context), // Note: We reuse 2.05 for 3.3. - StacksEpochId::Epoch33 => $Epoch205Version(args, env, context), + StacksEpochId::Epoch33 => $Epoch205Version(args, exec_state, invoke_ctx, context), // Note: We reuse 2.05 for 3.4. - StacksEpochId::Epoch34 => $Epoch205Version(args, env, context), + StacksEpochId::Epoch34 => $Epoch205Version(args, exec_state, invoke_ctx, context), } } }; @@ -150,7 +152,7 @@ define_versioned_named_enum_with_max!(NativeFunctions(ClarityVersion) { AsContract("as-contract", ClarityVersion::Clarity1, Some(ClarityVersion::Clarity3)), ContractOf("contract-of", ClarityVersion::Clarity1, None), PrincipalOf("principal-of?", ClarityVersion::Clarity1, None), - AtBlock("at-block", ClarityVersion::Clarity1, None), + AtBlock("at-block", ClarityVersion::Clarity1, Some(ClarityVersion::Clarity4)), GetBlockInfo("get-block-info?", ClarityVersion::Clarity1, Some(ClarityVersion::Clarity2)), GetBurnBlockInfo("get-burn-block-info?", ClarityVersion::Clarity2, None), ConsError("err", ClarityVersion::Clarity1, None), @@ -601,7 +603,11 @@ pub fn lookup_reserved_functions(name: &str, version: &ClarityVersion) -> Option } } -fn native_eq(args: Vec, env: &mut Environment) -> Result { +fn native_eq( + args: Vec, + exec_state: &mut ExecutionState, + _invoke_ctx: &InvocationContext, +) -> Result { // TODO: this currently uses the derived equality checks of Value, // however, that's probably not how we want to implement equality // checks on the ::ListTypes @@ -614,7 +620,7 @@ fn native_eq(args: Vec, env: &mut Environment) -> Result) -> Result { fn special_print( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { let arg = args.first().ok_or_else(|| { VmInternalError::BadSymbolicRepresentation("Print should have an argument".into()) })?; - let input = eval(arg, env, context)?; + let input = eval(arg, exec_state, invoke_ctx, context)?; - runtime_cost(ClarityCostFunction::Print, env, input.size()?)?; + runtime_cost( + ClarityCostFunction::Print, + exec_state, + input.as_ref().size()?, + )?; if cfg!(feature = "developer-mode") { - debug!("{}", &input); + debug!("{}", input.as_ref()); } - env.register_print_event(input.clone())?; - Ok(input) + let value = input.clone_with_cost(exec_state)?; + exec_state.register_print_event(invoke_ctx, value.clone())?; + Ok(value) } fn special_if( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(3, args)?; - runtime_cost(ClarityCostFunction::If, env, 0)?; + runtime_cost(ClarityCostFunction::If, exec_state, 0)?; // handle the conditional clause. - let conditional = eval(&args[0], env, context)?; - match conditional { + let conditional = eval(&args[0], exec_state, invoke_ctx, context)?; + match conditional.as_ref() { Value::Bool(result) => { - if result { - eval(&args[1], env, context) + if *result { + eval(&args[1], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state) } else { - eval(&args[2], env, context) + eval(&args[2], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state) } } _ => Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BoolType), - Box::new(conditional), + conditional.as_ref().to_error_string(), ) .into()), } @@ -684,27 +697,29 @@ fn special_if( fn special_asserts( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; - runtime_cost(ClarityCostFunction::Asserts, env, 0)?; + runtime_cost(ClarityCostFunction::Asserts, exec_state, 0)?; // handle the conditional clause. - let conditional = eval(&args[0], env, context)?; + let conditional = eval(&args[0], exec_state, invoke_ctx, context)?; - match conditional { + match conditional.as_ref() { Value::Bool(result) => { - if result { - Ok(conditional) + if *result { + conditional.clone_with_cost(exec_state) } else { - let thrown = eval(&args[1], env, context)?; + let thrown = + eval(&args[1], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; Err(EarlyReturnError::AssertionFailed(Box::new(thrown)).into()) } } _ => Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BoolType), - Box::new(conditional), + conditional.as_ref().to_error_string(), ) .into()), } @@ -749,20 +764,30 @@ where pub fn parse_eval_bindings( bindings: &[SymbolicExpression], binding_error_type: SyntaxBindingErrorType, - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result, VmExecutionError> { let mut result = Vec::with_capacity(bindings.len()); - handle_binding_list(bindings, binding_error_type, |var_name, var_sexp| { - eval(var_sexp, env, context).map(|value| result.push((var_name.clone(), value))) - })?; + + handle_binding_list( + bindings, + binding_error_type, + |var_name, var_sexp| -> Result<(), VmExecutionError> { + let value = + eval(var_sexp, exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; + result.push((var_name.clone(), value)); + Ok(()) + }, + )?; Ok(result) } fn special_let( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { // (let ((x 1) (y 2)) (+ x y)) -> 3 @@ -777,27 +802,28 @@ fn special_let( "Bad let syntax".to_string(), ))?; - runtime_cost(ClarityCostFunction::Let, env, bindings.len())?; + runtime_cost(ClarityCostFunction::Let, exec_state, bindings.len())?; // create a new context. let mut inner_context = context.extend()?; let mut memory_use = 0; - finally_drop_memory!( env, memory_use; { + finally_drop_memory!( exec_state, memory_use; { handle_binding_list::<_, VmExecutionError>(bindings, SyntaxBindingErrorType::Let, |binding_name, var_sexp| { - if is_reserved(binding_name, env.contract_context.get_clarity_version()) || - env.contract_context.lookup_function(binding_name).is_some() || + if is_reserved(binding_name, invoke_ctx.contract_context.get_clarity_version()) || + invoke_ctx.contract_context.lookup_function(binding_name).is_some() || inner_context.lookup_variable(binding_name).is_some() { return Err(RuntimeCheckErrorKind::NameAlreadyUsed(binding_name.clone().into()).into()) } - let binding_value = eval(var_sexp, env, &inner_context)?; + let binding_value = eval(var_sexp, exec_state, invoke_ctx, &inner_context)?; - let bind_mem_use = binding_value.get_memory_use()?; - env.add_memory(bind_mem_use)?; + let bind_mem_use = binding_value.as_ref().get_memory_use()?; + exec_state.add_memory(bind_mem_use)?; memory_use += bind_mem_use; // no check needed, b/c it's done in add_memory. - if *env.contract_context.get_clarity_version() >= ClarityVersion::Clarity2 && let CallableContract(trait_data) = &binding_value { + let binding_value = binding_value.clone_with_cost(exec_state)?; + if *invoke_ctx.contract_context.get_clarity_version() >= ClarityVersion::Clarity2 && let CallableContract(trait_data) = &binding_value { inner_context.callable_contracts.insert(binding_name.clone(), trait_data.clone()); } inner_context.variables.insert(binding_name.clone(), binding_value); @@ -807,17 +833,18 @@ fn special_let( // evaluate the let-bodies let mut last_result = None; for body in args[1..].iter() { - let body_result = eval(body, env, &inner_context)?; + let body_result = eval(body, exec_state, invoke_ctx, &inner_context)?; last_result.replace(body_result); } // last_result should always be Some(...), because of the arg len check above. - last_result.ok_or_else(|| VmInternalError::Expect("Failed to get let result".into()).into()) + last_result.ok_or_else(|| VmExecutionError::from(VmInternalError::Expect("Failed to get let result".into())))?.clone_with_cost(exec_state) }) } fn special_as_contract( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { // (as-contract (..)) @@ -825,33 +852,38 @@ fn special_as_contract( check_argument_count(1, args)?; // in epoch 2.1 and later, this has a cost - if *env.epoch() >= StacksEpochId::Epoch21 { - runtime_cost(ClarityCostFunction::AsContract, env, 0)?; + if *exec_state.epoch() >= StacksEpochId::Epoch21 { + runtime_cost(ClarityCostFunction::AsContract, exec_state, 0)?; } // nest an environment. - env.add_memory(cost_constants::AS_CONTRACT_MEMORY)?; + exec_state.add_memory(cost_constants::AS_CONTRACT_MEMORY)?; - let contract_principal = env.contract_context.contract_identifier.clone().into(); - let mut nested_env = env.nest_as_principal(contract_principal); + let contract_principal = invoke_ctx + .contract_context + .contract_identifier + .clone() + .into(); + let nested_view = invoke_ctx.with_principal(contract_principal); - let result = eval(&args[0], &mut nested_env, context); + let result = eval(&args[0], exec_state, &nested_view, context); - env.drop_memory(cost_constants::AS_CONTRACT_MEMORY)?; + exec_state.drop_memory(cost_constants::AS_CONTRACT_MEMORY)?; - result + result?.clone_with_cost(exec_state) } fn special_contract_of( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + _invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { // (contract-of (..)) // arg0 => trait check_argument_count(1, args)?; - runtime_cost(ClarityCostFunction::ContractOf, env, 0)?; + runtime_cost(ClarityCostFunction::ContractOf, exec_state, 0)?; let contract_ref = match &args[0].expr { SymbolicExpressionType::Atom(contract_ref) => contract_ref, @@ -865,7 +897,8 @@ fn special_contract_of( let contract_identifier = match context.lookup_callable_contract(contract_ref) { Some(trait_data) => { - env.global_context + exec_state + .global_context .database .get_contract(&trait_data.contract_identifier) .map_err(|_e| { @@ -895,6 +928,7 @@ mod test { use stacks_common::types::StacksEpochId; use super::ClarityVersion; + use crate::vm::contexts::{ExecutionState, InvocationContext}; use crate::vm::costs::LimitedCostTracker; use crate::vm::database::MemoryBackingStore; use crate::vm::errors::VmExecutionError; @@ -906,8 +940,7 @@ mod test { use crate::vm::tests::test_clarity_versions; use crate::vm::types::QualifiedContractIdentifier; use crate::vm::{ - CallStack, ContractContext, Environment, GlobalContext, LocalContext, SymbolicExpression, - Value, + CallStack, ContractContext, GlobalContext, LocalContext, SymbolicExpression, Value, }; /// Tests that if somehow we bypass static analysis checks, contract_of will return @@ -934,16 +967,19 @@ mod test { let context = LocalContext::new(); let mut call_stack = CallStack::new(); - let mut env = Environment::new( - &mut global_context, - &contract_context, - &mut call_stack, - None, - None, - None, - ); + let mut exec_state = ExecutionState { + global_context: &mut global_context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { + contract_context: &contract_context, + sender: None, + caller: None, + sponsor: None, + }; - let err = special_contract_of(&[non_atom], &mut env, &context).unwrap_err(); + let err = + special_contract_of(&[non_atom], &mut exec_state, &invoke_ctx, &context).unwrap_err(); assert_eq!( err, VmExecutionError::RuntimeCheck(RuntimeCheckErrorKind::Unreachable( @@ -978,16 +1014,18 @@ mod test { let context = LocalContext::new(); let mut call_stack = CallStack::new(); - let mut env = Environment::new( - &mut global_context, - &contract_context, - &mut call_stack, - None, - None, - None, - ); + let mut exec_state = ExecutionState { + global_context: &mut global_context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { + contract_context: &contract_context, + sender: None, + caller: None, + sponsor: None, + }; - let err = special_contract_of(&[atom], &mut env, &context).unwrap_err(); + let err = special_contract_of(&[atom], &mut exec_state, &invoke_ctx, &context).unwrap_err(); assert_eq!( err, @@ -1022,16 +1060,18 @@ mod test { let context = LocalContext::new(); let mut call_stack = CallStack::new(); - let mut env = Environment::new( - &mut global_context, - &contract_context, - &mut call_stack, - None, - None, - None, - ); + let mut exec_state = ExecutionState { + global_context: &mut global_context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { + contract_context: &contract_context, + sender: None, + caller: None, + sponsor: None, + }; - let err = special_let(&args, &mut env, &context).unwrap_err(); + let err = special_let(&args, &mut exec_state, &invoke_ctx, &context).unwrap_err(); assert_eq!( VmExecutionError::RuntimeCheck(RuntimeCheckErrorKind::Unreachable( @@ -1069,16 +1109,19 @@ mod test { let context = LocalContext::new(); let mut call_stack = CallStack::new(); - let mut env = Environment::new( - &mut global_context, - &contract_context, - &mut call_stack, - None, - None, - None, - ); + let mut exec_state = ExecutionState { + global_context: &mut global_context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { + contract_context: &contract_context, + sender: None, + caller: None, + sponsor: None, + }; - let err = special_get_tenure_info(&args, &mut env, &context).unwrap_err(); + let err = + special_get_tenure_info(&args, &mut exec_state, &invoke_ctx, &context).unwrap_err(); assert_eq!( err, @@ -1118,16 +1161,19 @@ mod test { let context = LocalContext::new(); let mut call_stack = CallStack::new(); - let mut env = Environment::new( - &mut global_context, - &contract_context, - &mut call_stack, - None, - None, - None, - ); + let mut exec_state = ExecutionState { + global_context: &mut global_context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { + contract_context: &contract_context, + sender: None, + caller: None, + sponsor: None, + }; - let err = special_get_burn_block_info(&args, &mut env, &context).unwrap_err(); + let err = + special_get_burn_block_info(&args, &mut exec_state, &invoke_ctx, &context).unwrap_err(); assert_eq!( err, @@ -1165,16 +1211,19 @@ mod test { let context = LocalContext::new(); let mut call_stack = CallStack::new(); - let mut env = Environment::new( - &mut global_context, - &contract_context, - &mut call_stack, - None, - None, - None, - ); + let mut exec_state = ExecutionState { + global_context: &mut global_context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { + contract_context: &contract_context, + sender: None, + caller: None, + sponsor: None, + }; - let err = special_get_stacks_block_info(&args, &mut env, &context).unwrap_err(); + let err = special_get_stacks_block_info(&args, &mut exec_state, &invoke_ctx, &context) + .unwrap_err(); assert_eq!( err, @@ -1213,16 +1262,19 @@ mod test { let context = LocalContext::new(); let mut call_stack = CallStack::new(); - let mut env = Environment::new( - &mut global_context, - &contract_context, - &mut call_stack, - None, - None, - None, - ); + let mut exec_state = ExecutionState { + global_context: &mut global_context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { + contract_context: &contract_context, + sender: None, + caller: None, + sponsor: None, + }; - let err = special_get_stacks_block_info(&args, &mut env, &context).unwrap_err(); + let err = special_get_stacks_block_info(&args, &mut exec_state, &invoke_ctx, &context) + .unwrap_err(); assert_eq!( err, @@ -1262,16 +1314,19 @@ mod test { let context = LocalContext::new(); let mut call_stack = CallStack::new(); - let mut env = Environment::new( - &mut global_context, - &contract_context, - &mut call_stack, - None, - None, - None, - ); + let mut exec_state = ExecutionState { + global_context: &mut global_context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { + contract_context: &contract_context, + sender: None, + caller: None, + sponsor: None, + }; - let err = special_get_burn_block_info(&args, &mut env, &context).unwrap_err(); + let err = + special_get_burn_block_info(&args, &mut exec_state, &invoke_ctx, &context).unwrap_err(); assert_eq!( err, @@ -1301,22 +1356,23 @@ mod test { let context = LocalContext::new(); // EMPTY — no callable_contracts let mut call_stack = CallStack::new(); - let mut env = Environment::new( - &mut global_context, - &contract_context, - &mut call_stack, - None, - None, - None, - ); - + let mut exec_state = ExecutionState { + global_context: &mut global_context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { + contract_context: &contract_context, + sender: None, + caller: None, + sponsor: None, + }; // (contract-call? unknown-contract foo) let args = vec![ SymbolicExpression::atom("unknown-contract".into()), // Atom, NOT registered SymbolicExpression::atom("foo".into()), // Valid function name atom ]; - let err = special_contract_call(&args, &mut env, &context).unwrap_err(); + let err = special_contract_call(&args, &mut exec_state, &invoke_ctx, &context).unwrap_err(); assert_eq!( err, diff --git a/clarity/src/vm/functions/options.rs b/clarity/src/vm/functions/options.rs index c0771d5a5f3..c338fd2efe4 100644 --- a/clarity/src/vm/functions/options.rs +++ b/clarity/src/vm/functions/options.rs @@ -15,7 +15,7 @@ // along with this program. If not, see . use crate::vm::Value::CallableContract; -use crate::vm::contexts::{Environment, LocalContext}; +use crate::vm::contexts::{ExecutionState, InvocationContext, LocalContext}; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::{CostTracker, MemoryConsumer, runtime_cost}; use crate::vm::errors::{ @@ -123,21 +123,27 @@ fn eval_with_new_binding( body: &SymbolicExpression, bind_name: ClarityName, bind_value: Value, - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { let mut inner_context = context.extend()?; - if vm::is_reserved(&bind_name, env.contract_context.get_clarity_version()) - || env.contract_context.lookup_function(&bind_name).is_some() + if vm::is_reserved( + &bind_name, + invoke_ctx.contract_context.get_clarity_version(), + ) || invoke_ctx + .contract_context + .lookup_function(&bind_name) + .is_some() || inner_context.lookup_variable(&bind_name).is_some() { return Err(RuntimeCheckErrorKind::NameAlreadyUsed(bind_name.into()).into()); } let memory_use = bind_value.get_memory_use()?; - env.add_memory(memory_use)?; + exec_state.add_memory(memory_use)?; - if *env.contract_context.get_clarity_version() >= ClarityVersion::Clarity2 + if *invoke_ctx.contract_context.get_clarity_version() >= ClarityVersion::Clarity2 && let CallableContract(trait_data) = &bind_value { inner_context.callable_contracts.insert( @@ -149,9 +155,10 @@ fn eval_with_new_binding( ); } inner_context.variables.insert(bind_name, bind_value); - let result = vm::eval(body, env, &inner_context); + let result = vm::eval(body, exec_state, invoke_ctx, &inner_context) + .and_then(|v| v.clone_with_cost(exec_state)); - env.drop_memory(memory_use)?; + exec_state.drop_memory(memory_use)?; result } @@ -159,7 +166,8 @@ fn eval_with_new_binding( fn special_match_opt( input: OptionalData, args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { if args.len() != 3 { @@ -179,15 +187,24 @@ fn special_match_opt( let none_branch = &args[2]; match input.data { - Some(data) => eval_with_new_binding(some_branch, bind_name, *data, env, context), - None => vm::eval(none_branch, env, context), + Some(data) => eval_with_new_binding( + some_branch, + bind_name, + *data, + exec_state, + invoke_ctx, + context, + ), + None => vm::eval(none_branch, exec_state, invoke_ctx, context) + .and_then(|v| v.clone_with_cost(exec_state)), } } fn special_match_resp( input: ResponseData, args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { if args.len() != 4 { @@ -217,26 +234,46 @@ fn special_match_resp( let err_branch = &args[3]; if input.committed { - eval_with_new_binding(ok_branch, ok_bind_name, *input.data, env, context) + eval_with_new_binding( + ok_branch, + ok_bind_name, + *input.data, + exec_state, + invoke_ctx, + context, + ) } else { - eval_with_new_binding(err_branch, err_bind_name, *input.data, env, context) + eval_with_new_binding( + err_branch, + err_bind_name, + *input.data, + exec_state, + invoke_ctx, + context, + ) } } pub fn special_match( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_arguments_at_least(1, args)?; - let input = vm::eval(&args[0], env, context)?; + let input = + { vm::eval(&args[0], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)? }; - runtime_cost(ClarityCostFunction::Match, env, 0)?; + runtime_cost(ClarityCostFunction::Match, exec_state, 0)?; match input { - Value::Response(data) => special_match_resp(data, &args[1..], env, context), - Value::Optional(data) => special_match_opt(data, &args[1..], env, context), + Value::Response(data) => { + special_match_resp(data, &args[1..], exec_state, invoke_ctx, context) + } + Value::Optional(data) => { + special_match_opt(data, &args[1..], exec_state, invoke_ctx, context) + } _ => Err(RuntimeCheckErrorKind::Unreachable(format!( "Bad match input: {}", TypeSignature::type_of(&input)? diff --git a/clarity/src/vm/functions/post_conditions.rs b/clarity/src/vm/functions/post_conditions.rs index 771e2d0906f..407803a4c32 100644 --- a/clarity/src/vm/functions/post_conditions.rs +++ b/clarity/src/vm/functions/post_conditions.rs @@ -20,7 +20,7 @@ use clarity_types::types::{AssetIdentifier, PrincipalData, StandardPrincipalData use stacks_common::types::StacksEpochId; use crate::vm::analysis::type_checker::v2_1::natives::post_conditions::MAX_ALLOWANCES; -use crate::vm::contexts::AssetMap; +use crate::vm::contexts::{AssetMap, ExecutionState, InvocationContext}; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::{CostTracker, MemoryConsumer, constants as cost_constants, runtime_cost}; use crate::vm::errors::{ @@ -30,7 +30,7 @@ use crate::vm::errors::{ use crate::vm::functions::NativeFunctions; use crate::vm::representations::SymbolicExpression; use crate::vm::types::Value; -use crate::vm::{Environment, LocalContext, eval}; +use crate::vm::{LocalContext, eval}; #[derive(Debug)] pub struct StxAllowance { @@ -97,7 +97,8 @@ impl Allowance { fn eval_allowance( allowance_expr: &SymbolicExpression, - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { let list = allowance_expr @@ -117,7 +118,7 @@ fn eval_allowance( ))?; let Some(ref native_function) = NativeFunctions::lookup_by_name_at_version( name, - env.contract_context.get_clarity_version(), + invoke_ctx.contract_context.get_clarity_version(), ) else { return Err( RuntimeCheckErrorKind::Unreachable(format!("Expected allowance expr: {name}")).into(), @@ -129,10 +130,13 @@ fn eval_allowance( if rest.len() != 1 { return Err(RuntimeCheckErrorKind::IncorrectArgumentCount(1, rest.len()).into()); } - let amount = eval(&rest[0], env, context)?; - let amount = amount - .expect_u128() - .map_err(|_| VmInternalError::Expect("Expected u128".into()))?; + let amount = eval(&rest[0], exec_state, invoke_ctx, context)?; + let amount = match amount.as_ref() { + Value::UInt(amount) => *amount, + _ => { + return Err(VmInternalError::Expect("Expected u128".into()).into()); + } + }; Ok(Allowance::Stx(StxAllowance { amount })) } NativeFunctions::AllowanceWithFt => { @@ -140,7 +144,8 @@ fn eval_allowance( return Err(RuntimeCheckErrorKind::IncorrectArgumentCount(3, rest.len()).into()); } - let contract_value = eval(&rest[0], env, context)?; + let contract_value = + eval(&rest[0], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; let contract = contract_value .clone() .expect_principal() @@ -148,14 +153,15 @@ fn eval_allowance( let contract_identifier = match contract { PrincipalData::Standard(_) => { return Err(RuntimeCheckErrorKind::ExpectedContractPrincipalValue( - contract_value.into(), + contract_value.to_error_string(), ) .into()); } PrincipalData::Contract(c) => c, }; - let asset_name = eval(&rest[1], env, context)?; + let asset_name = + eval(&rest[1], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; let asset_name = asset_name .expect_string_ascii() .map_err(|_| VmInternalError::Expect("Expected ASCII String.".into()))?; @@ -171,7 +177,8 @@ fn eval_allowance( asset_name, }; - let amount = eval(&rest[2], env, context)?; + let amount = + eval(&rest[2], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; let amount = amount .expect_u128() .map_err(|_| VmInternalError::Expect("Expected u128".into()))?; @@ -183,7 +190,8 @@ fn eval_allowance( return Err(RuntimeCheckErrorKind::IncorrectArgumentCount(3, rest.len()).into()); } - let contract_value = eval(&rest[0], env, context)?; + let contract_value = + eval(&rest[0], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; let contract = contract_value .clone() .expect_principal() @@ -191,14 +199,15 @@ fn eval_allowance( let contract_identifier = match contract { PrincipalData::Standard(_) => { return Err(RuntimeCheckErrorKind::ExpectedContractPrincipalValue( - contract_value.into(), + contract_value.to_error_string(), ) .into()); } PrincipalData::Contract(c) => c, }; - let asset_name = eval(&rest[1], env, context)?; + let asset_name = + eval(&rest[1], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; let asset_name = asset_name .expect_string_ascii() .map_err(|_| VmInternalError::Expect("Expected ASCII String.".into()))?; @@ -214,7 +223,8 @@ fn eval_allowance( asset_name, }; - let asset_id_list = eval(&rest[2], env, context)?; + let asset_id_list = + eval(&rest[2], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; let asset_ids = asset_id_list .expect_list() .map_err(|_| VmInternalError::Expect("Expected list".into()))?; @@ -225,7 +235,8 @@ fn eval_allowance( if rest.len() != 1 { return Err(RuntimeCheckErrorKind::IncorrectArgumentCount(1, rest.len()).into()); } - let amount = eval(&rest[0], env, context)?; + let amount = + eval(&rest[0], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; let amount = amount .expect_u128() .map_err(|_| VmInternalError::Expect("Expected u128".into()))?; @@ -246,7 +257,8 @@ fn eval_allowance( /// Handles the function `restrict-assets?` pub fn special_restrict_assets( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { // (restrict-assets? asset-owner ((with-stx|with-ft|with-nft|with-stacking)*) expr-body1 expr-body2 ... expr-body-last) @@ -263,13 +275,18 @@ pub fn special_restrict_assets( ))?; let body_exprs = &args[2..]; - let asset_owner = eval(asset_owner_expr, env, context)?; + let asset_owner = + eval(asset_owner_expr, exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; let asset_owner = asset_owner .expect_principal() .map_err(|_| VmInternalError::Expect("Expected principal".into()))?; let allowance_len = allowance_list.len(); - runtime_cost(ClarityCostFunction::RestrictAssets, env, allowance_len)?; + runtime_cost( + ClarityCostFunction::RestrictAssets, + exec_state, + allowance_len, + )?; if allowance_len > MAX_ALLOWANCES { return Err(RuntimeCheckErrorKind::Unreachable(format!( @@ -280,26 +297,27 @@ pub fn special_restrict_assets( let mut allowances = Vec::with_capacity(allowance_len); for allowance in allowance_list { - allowances.push(eval_allowance(allowance, env, context)?); + allowances.push(eval_allowance(allowance, exec_state, invoke_ctx, context)?); } // Create a new evaluation context, so that we can rollback if the // post-conditions are violated - let epoch = *env.epoch(); - env.global_context.begin(); + let epoch = *exec_state.epoch(); + exec_state.global_context.begin(); // Evaluate the body expressions inside a closure so `?` only exits the closure let eval_result: Result, VmExecutionError> = (|| -> Result, VmExecutionError> { let mut last_result = None; for expr in body_exprs { - let result = eval(expr, env, context)?; + let result = + eval(expr, exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; last_result.replace(result); } Ok(last_result) })(); - let asset_maps = env.global_context.get_readonly_asset_map()?; + let asset_maps = exec_state.global_context.get_readonly_asset_map()?; // If the allowances are violated: // - Rollback the context @@ -307,16 +325,16 @@ pub fn special_restrict_assets( match check_allowances(&asset_owner, allowances, asset_maps, epoch) { Ok(None) => {} Ok(Some(violation_index)) => { - env.global_context.roll_back()?; + exec_state.global_context.roll_back()?; return Ok(Value::error(Value::UInt(violation_index))?); } Err(e) => { - env.global_context.roll_back()?; + exec_state.global_context.roll_back()?; return Err(e); } } - env.global_context.commit()?; + exec_state.global_context.commit()?; // No allowance violation, so handle the result of the body evaluation match eval_result { @@ -338,7 +356,8 @@ pub fn special_restrict_assets( /// Handles the function `as-contract?` pub fn special_as_contract( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { // (as-contract? ((with-stx|with-ft|with-nft|with-stacking)*) expr-body1 expr-body2 ... expr-body-last) @@ -355,45 +374,45 @@ pub fn special_as_contract( runtime_cost( ClarityCostFunction::AsContractSafe, - env, + exec_state, allowance_list.len(), )?; let mut memory_use = 0u64; - finally_drop_memory!( env, memory_use; { + finally_drop_memory!( exec_state, memory_use; { let mut allowances = Vec::with_capacity(allowance_list.len()); for allowance_expr in allowance_list { - let allowance = eval_allowance(allowance_expr, env, context)?; + let allowance = eval_allowance(allowance_expr, exec_state, invoke_ctx, context)?; let allowance_memory = u64::try_from(allowance.size_in_bytes()?) .map_err(|_| VmInternalError::Expect("Allowance size too large".into()))?; - env.add_memory(allowance_memory)?; + exec_state.add_memory(allowance_memory)?; memory_use += allowance_memory; allowances.push(allowance); } - env.add_memory(cost_constants::AS_CONTRACT_MEMORY)?; + exec_state.add_memory(cost_constants::AS_CONTRACT_MEMORY)?; memory_use += cost_constants::AS_CONTRACT_MEMORY; - let contract_principal: PrincipalData = env.contract_context.contract_identifier.clone().into(); - let epoch = *env.epoch(); - let mut nested_env = env.nest_as_principal(contract_principal.clone()); + let contract_principal: PrincipalData = invoke_ctx.contract_context.contract_identifier.clone().into(); + let epoch = *exec_state.epoch(); + let nested_view = invoke_ctx.with_principal(contract_principal.clone()); // Create a new evaluation context, so that we can rollback if the // post-conditions are violated - nested_env.global_context.begin(); + exec_state.global_context.begin(); // Evaluate the body expressions inside a closure so `?` only exits the closure let eval_result: Result, VmExecutionError> = (|| -> Result, VmExecutionError> { let mut last_result = None; for expr in body_exprs { - let result = eval(expr, &mut nested_env, context)?; + let result = eval(expr, exec_state, &nested_view, context)?.clone_with_cost(exec_state)?; last_result.replace(result); } Ok(last_result) })(); - let asset_maps = nested_env.global_context.get_readonly_asset_map()?; + let asset_maps = exec_state.global_context.get_readonly_asset_map()?; // If the allowances are violated: // - Rollback the context @@ -406,16 +425,16 @@ pub fn special_as_contract( ) { Ok(None) => {} Ok(Some(violation_index)) => { - nested_env.global_context.roll_back()?; + exec_state.global_context.roll_back()?; return Ok(Value::error(Value::UInt(violation_index))?); } Err(e) => { - nested_env.global_context.roll_back()?; + exec_state.global_context.roll_back()?; return Err(e); } } - nested_env.global_context.commit()?; + exec_state.global_context.commit()?; // No allowance violation, so handle the result of the body evaluation match eval_result { @@ -637,7 +656,8 @@ fn check_allowances( /// by the above `eval_allowance` function. pub fn special_allowance( _args: &[SymbolicExpression], - _env: &mut Environment, + _env: &mut ExecutionState, + _invoke_ctx: &InvocationContext, _context: &LocalContext, ) -> Result { Err(RuntimeCheckErrorKind::Unreachable("Allowance expr not allowed".to_string()).into()) @@ -679,16 +699,19 @@ mod test { let context = LocalContext::new(); let mut call_stack = CallStack::new(); - let mut env = Environment::new( - &mut global_context, - &contract_context, - &mut call_stack, - None, - None, - None, - ); - let err = eval_allowance(&allowance_expr, &mut env, &context).unwrap_err(); + let mut exec_state = ExecutionState { + global_context: &mut global_context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { + contract_context: &contract_context, + sender: None, + caller: None, + sponsor: None, + }; + let err = + eval_allowance(&allowance_expr, &mut exec_state, &invoke_ctx, &context).unwrap_err(); assert_eq!( VmExecutionError::RuntimeCheck(RuntimeCheckErrorKind::Unreachable( diff --git a/clarity/src/vm/functions/principals.rs b/clarity/src/vm/functions/principals.rs index a2fc754efb9..0c83dec996d 100644 --- a/clarity/src/vm/functions/principals.rs +++ b/clarity/src/vm/functions/principals.rs @@ -17,7 +17,7 @@ use stacks_common::address::{ C32_ADDRESS_VERSION_TESTNET_MULTISIG, C32_ADDRESS_VERSION_TESTNET_SINGLESIG, }; -use crate::vm::contexts::GlobalContext; +use crate::vm::contexts::{ExecutionState, GlobalContext, InvocationContext}; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::runtime_cost; use crate::vm::errors::{ @@ -31,7 +31,7 @@ use crate::vm::types::{ ASCIIData, BuffData, CharType, OptionalData, PrincipalData, QualifiedContractIdentifier, ResponseData, SequenceData, StandardPrincipalData, TupleData, TypeSignature, Value, }; -use crate::vm::{ContractName, Environment, LocalContext, eval}; +use crate::vm::{ContractName, LocalContext, eval}; pub enum PrincipalConstructErrorCode { VERSION_BYTE = 0, @@ -64,26 +64,27 @@ fn version_matches_current_network(version: u8, global_context: &GlobalContext) pub fn special_is_standard( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(1, args)?; - runtime_cost(ClarityCostFunction::IsStandard, env, 0)?; - let owner = eval(&args[0], env, context)?; + runtime_cost(ClarityCostFunction::IsStandard, exec_state, 0)?; + let owner = eval(&args[0], exec_state, invoke_ctx, context)?; - let version = if let Value::Principal(ref p) = owner { + let version = if let Value::Principal(p) = owner.as_ref() { p.version() } else { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::PrincipalType), - Box::new(owner), + owner.as_ref().to_error_string(), ) .into()); }; Ok(Value::Bool(version_matches_current_network( version, - env.global_context, + exec_state.global_context, ))) } @@ -166,13 +167,14 @@ fn create_principal_value_error_response( pub fn special_principal_destruct( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(1, args)?; - runtime_cost(ClarityCostFunction::PrincipalDestruct, env, 0)?; + runtime_cost(ClarityCostFunction::PrincipalDestruct, exec_state, 0)?; - let principal = eval(&args[0], env, context)?; + let principal = eval(&args[0], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; let (version_byte, hash_bytes, name_opt) = match principal { Value::Principal(PrincipalData::Standard(p)) => { @@ -186,7 +188,7 @@ pub fn special_principal_destruct( _ => { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::PrincipalType), - Box::new(principal), + principal.to_error_string(), ) .into()); } @@ -194,7 +196,8 @@ pub fn special_principal_destruct( // `version_byte_is_valid` determines whether the returned `Response` is through the success // channel or the error channel. - let version_byte_is_valid = version_matches_current_network(version_byte, env.global_context); + let version_byte_is_valid = + version_matches_current_network(version_byte, exec_state.global_context); let tuple = create_principal_destruct_tuple(version_byte, &hash_bytes, name_opt)?; Ok(Value::Response(ResponseData { @@ -205,30 +208,31 @@ pub fn special_principal_destruct( pub fn special_principal_construct( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_arguments_at_least(2, args)?; check_arguments_at_most(3, args)?; - runtime_cost(ClarityCostFunction::PrincipalConstruct, env, 0)?; + runtime_cost(ClarityCostFunction::PrincipalConstruct, exec_state, 0)?; - let version = eval(&args[0], env, context)?; - let hash_bytes = eval(&args[1], env, context)?; + let version = eval(&args[0], exec_state, invoke_ctx, context)?; + let hash_bytes = eval(&args[1], exec_state, invoke_ctx, context)?; let name_opt = if args.len() > 2 { - Some(eval(&args[2], env, context)?) + Some(eval(&args[2], exec_state, invoke_ctx, context)?) } else { None }; // Check the version byte. - let verified_version = match version { - Value::Sequence(SequenceData::Buffer(BuffData { ref data })) => data, + let verified_version = match version.as_ref() { + Value::Sequence(SequenceData::Buffer(BuffData { data })) => data, _ => { return { // This is an aborting error because this should have been caught in analysis pass. Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_1), - Box::new(version), + version.as_ref().to_error_string(), ) .into()) }; @@ -239,7 +243,7 @@ pub fn special_principal_construct( // should have been caught by the type-checker return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_1), - Box::new(version), + version.as_ref().to_error_string(), ) .into()); } else if verified_version.is_empty() { @@ -258,16 +262,17 @@ pub fn special_principal_construct( // `version_byte_is_valid` determines whether the returned `Response` is through the success // channel or the error channel. - let version_byte_is_valid = version_matches_current_network(version_byte, env.global_context); + let version_byte_is_valid = + version_matches_current_network(version_byte, exec_state.global_context); // Check the hash bytes -- they must be a (buff 20). // This is an aborting error because this should have been caught in analysis pass. - let verified_hash_bytes = match hash_bytes { - Value::Sequence(SequenceData::Buffer(BuffData { ref data })) => data, + let verified_hash_bytes = match hash_bytes.as_ref() { + Value::Sequence(SequenceData::Buffer(BuffData { data })) => data, _ => { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_20), - Box::new(hash_bytes), + hash_bytes.as_ref().to_error_string(), ) .into()); } @@ -278,7 +283,7 @@ pub fn special_principal_construct( if verified_hash_bytes.len() > 20 { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_20), - Box::new(hash_bytes), + hash_bytes.as_ref().to_error_string(), ) .into()); } @@ -298,12 +303,12 @@ pub fn special_principal_construct( let principal = if let Some(name) = name_opt { // requested a contract principal. Verify that the `name` is a valid ContractName. // The type-checker will have verified that it's (string-ascii 40), but not long enough. - let name_bytes = match name { + let name_bytes = match name.clone_with_cost(exec_state)? { Value::Sequence(SequenceData::String(CharType::ASCII(ascii_data))) => ascii_data, - _ => { + name => { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::CONTRACT_NAME_STRING_ASCII_MAX), - Box::new(name), + name.to_error_string(), ) .into()); } @@ -320,7 +325,7 @@ pub fn special_principal_construct( if name_bytes.data.len() > CONTRACT_MAX_NAME_LENGTH { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::CONTRACT_NAME_STRING_ASCII_MAX), - Box::new(Value::from(name_bytes)), + Value::from(name_bytes).to_error_string(), ) .into()); } diff --git a/clarity/src/vm/functions/sequences.rs b/clarity/src/vm/functions/sequences.rs index 9db84b3e1b7..a85464c76d6 100644 --- a/clarity/src/vm/functions/sequences.rs +++ b/clarity/src/vm/functions/sequences.rs @@ -19,6 +19,7 @@ use std::cmp; use clarity_types::types::RetainValuesError; use stacks_common::types::StacksEpochId; +use crate::vm::contexts::{ExecutionState, InvocationContext}; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::{CostOverflowingMath, runtime_cost}; use crate::vm::errors::{ @@ -29,15 +30,20 @@ use crate::vm::representations::SymbolicExpression; use crate::vm::types::TypeSignature::BoolType; use crate::vm::types::signatures::ListTypeData; use crate::vm::types::{ListData, SequenceData, TypeSignature, Value}; -use crate::vm::{Environment, LocalContext, apply, eval, lookup_function}; +use crate::vm::{LocalContext, apply, eval, lookup_function}; pub fn list_cons( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { - let eval_tried: Result, VmExecutionError> = - args.iter().map(|x| eval(x, env, context)).collect(); + let eval_tried: Result, VmExecutionError> = args + .iter() + .map(|x| { + eval(x, exec_state, invoke_ctx, context).and_then(|v| v.clone_with_cost(exec_state)) + }) + .collect(); let args = eval_tried?; let mut arg_size = 0; @@ -45,20 +51,21 @@ pub fn list_cons( arg_size = arg_size.cost_overflow_add(a.size()?.into())?; } - runtime_cost(ClarityCostFunction::ListCons, env, arg_size)?; + runtime_cost(ClarityCostFunction::ListCons, exec_state, arg_size)?; - let value = Value::cons_list(args, env.epoch())?; + let value = Value::cons_list(args, exec_state.epoch())?; Ok(value) } pub fn special_filter( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; - runtime_cost(ClarityCostFunction::Filter, env, 0)?; + runtime_cost(ClarityCostFunction::Filter, exec_state, 0)?; let function_name = args[0] .match_atom() @@ -66,21 +73,23 @@ pub fn special_filter( "Expected name".to_string(), ))?; - let mut sequence = eval(&args[1], env, context)?; - let function = lookup_function(function_name, env)?; + let mut sequence = + eval(&args[1], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; + let function = lookup_function(function_name, exec_state, invoke_ctx)?; match sequence { Value::Sequence(ref mut sequence_data) => { sequence_data .retain_values( &mut |atom: SymbolicExpression| -> Result { - let filter_eval = apply(&function, &[atom], env, context)?; + let filter_eval = + apply(&function, &[atom], exec_state, invoke_ctx, context)?; if let Value::Bool(include) = filter_eval { Ok(include) } else { Err(RuntimeCheckErrorKind::TypeValueError( Box::new(BoolType), - Box::new(filter_eval), + filter_eval.to_error_string(), ) .into()) } @@ -108,12 +117,13 @@ pub fn special_filter( pub fn special_fold( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(3, args)?; - runtime_cost(ClarityCostFunction::Fold, env, 0)?; + runtime_cost(ClarityCostFunction::Fold, exec_state, 0)?; let function_name = args[0] .match_atom() @@ -121,9 +131,10 @@ pub fn special_fold( "Expected name".to_string(), ))?; - let function = lookup_function(function_name, env)?; - let mut sequence = eval(&args[1], env, context)?; - let initial = eval(&args[2], env, context)?; + let function = lookup_function(function_name, exec_state, invoke_ctx)?; + let mut sequence = + eval(&args[1], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; + let initial = eval(&args[2], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; match sequence { Value::Sequence(ref mut sequence_data) => sequence_data @@ -138,7 +149,8 @@ pub fn special_fold( apply( &function, &[x, SymbolicExpression::atom_value(acc)], - env, + exec_state, + invoke_ctx, context, ) }), @@ -152,19 +164,20 @@ pub fn special_fold( pub fn special_map( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_arguments_at_least(2, args)?; - runtime_cost(ClarityCostFunction::Map, env, args.len())?; + runtime_cost(ClarityCostFunction::Map, exec_state, args.len())?; let function_name = args[0] .match_atom() .ok_or(RuntimeCheckErrorKind::Unreachable( "Expected name".to_string(), ))?; - let function = lookup_function(function_name, env)?; + let function = lookup_function(function_name, exec_state, invoke_ctx)?; // Let's consider a function f (f a b c ...) // We will first re-arrange our sequences [a0, a1, ...] [b0, b1, ...] [c0, c1, ...] ... @@ -172,7 +185,8 @@ pub fn special_map( let mut mapped_func_args = vec![]; let mut min_args_len = usize::MAX; for map_arg in args[1..].iter() { - let mut sequence = eval(map_arg, env, context)?; + let mut sequence = + eval(map_arg, exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; match sequence { Value::Sequence(ref mut sequence_data) => { min_args_len = min_args_len.min(sequence_data.len()); @@ -218,43 +232,46 @@ pub fn special_map( } else { previous_len = Some(arguments.len()); } - let res = apply(&function, arguments, env, context)?; + let res = apply(&function, arguments, exec_state, invoke_ctx, context)?; mapped_results.push(res); } - let value = Value::cons_list(mapped_results, env.epoch())?; + let value = Value::cons_list(mapped_results, exec_state.epoch())?; Ok(value) } pub fn special_append( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; - let sequence = eval(&args[0], env, context)?; + let sequence = eval(&args[0], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; match sequence { Value::Sequence(SequenceData::List(list)) => { - let element = eval(&args[1], env, context)?; + let element = eval(&args[1], exec_state, invoke_ctx, context)?; let ListData { mut data, type_signature, } = list; let (entry_type, size) = type_signature.destruct(); - let element_type = TypeSignature::type_of(&element)?; + let element_type = TypeSignature::type_of(element.as_ref())?; runtime_cost( ClarityCostFunction::Append, - env, + exec_state, u64::from(cmp::max(entry_type.size()?, element_type.size()?)), )?; + let element = element.clone_with_cost(exec_state)?; if entry_type.is_no_type() { assert_eq!(size, 0); - return Ok(Value::cons_list(vec![element], env.epoch())?); + return Ok(Value::cons_list(vec![element], exec_state.epoch())?); } + let next_entry_type = - TypeSignature::least_supertype(env.epoch(), &entry_type, &element_type)?; - let (element, _) = Value::sanitize_value(env.epoch(), &next_entry_type, element) + TypeSignature::least_supertype(exec_state.epoch(), &entry_type, &element_type)?; + let (element, _) = Value::sanitize_value(exec_state.epoch(), &next_entry_type, element) .ok_or_else(|| RuntimeCheckErrorKind::ListTypesMustMatch)?; let next_type_signature = ListTypeData::new_list(next_entry_type, size + 1)?; @@ -274,22 +291,27 @@ switch_on_global_epoch!(special_concat(special_concat_v200, special_concat_v205) pub fn special_concat_v200( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; - let mut wrapped_seq = eval(&args[0], env, context)?; - let other_wrapped_seq = eval(&args[1], env, context)?; + let mut wrapped_seq = + eval(&args[0], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; + let other_wrapped_seq = + eval(&args[1], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; runtime_cost( ClarityCostFunction::Concat, - env, + exec_state, u64::from(wrapped_seq.size()?).cost_overflow_add(u64::from(other_wrapped_seq.size()?))?, )?; match (&mut wrapped_seq, other_wrapped_seq) { - (Value::Sequence(seq), Value::Sequence(other_seq)) => seq.concat(env.epoch(), other_seq)?, + (Value::Sequence(seq), Value::Sequence(other_seq)) => { + seq.concat(exec_state.epoch(), other_seq)? + } (Value::Sequence(_), other_value) => { // The first value is a sequence, but the second is not return Err(RuntimeCheckErrorKind::Unreachable(format!( @@ -313,34 +335,37 @@ pub fn special_concat_v200( pub fn special_concat_v205( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; - let mut wrapped_seq = eval(&args[0], env, context)?; - let other_wrapped_seq = eval(&args[1], env, context)?; + let mut wrapped_seq = + eval(&args[0], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; + let other_wrapped_seq = + eval(&args[1], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; match (&mut wrapped_seq, other_wrapped_seq) { (Value::Sequence(seq), Value::Sequence(other_seq)) => { runtime_cost( ClarityCostFunction::Concat, - env, + exec_state, (seq.len() as u64).cost_overflow_add(other_seq.len() as u64)?, )?; - seq.concat(env.epoch(), other_seq)? + seq.concat(exec_state.epoch(), other_seq)? } (Value::Sequence(seq_data), other_value) => { - runtime_cost(ClarityCostFunction::Concat, env, 1)?; + runtime_cost(ClarityCostFunction::Concat, exec_state, 1)?; return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(seq_data.type_signature()?), - Box::new(other_value), + other_value.to_error_string(), ) .into()); } _ => { - runtime_cost(ClarityCostFunction::Concat, env, 1)?; + runtime_cost(ClarityCostFunction::Concat, exec_state, 1)?; return Err(RuntimeCheckErrorKind::Unreachable(format!( "Expected sequence: {}", TypeSignature::type_of(&wrapped_seq)?, @@ -354,14 +379,16 @@ pub fn special_concat_v205( pub fn special_as_max_len( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(2, args)?; - let mut sequence = eval(&args[0], env, context)?; + let mut sequence = + eval(&args[0], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; - runtime_cost(ClarityCostFunction::AsMaxLen, env, 0)?; + runtime_cost(ClarityCostFunction::AsMaxLen, exec_state, 0)?; if let Some(Value::UInt(expected_len)) = args[1].match_literal_value() { let sequence_len = match sequence { @@ -383,10 +410,10 @@ pub fn special_as_max_len( Ok(Value::some(sequence)?) } } else { - let actual_len = eval(&args[1], env, context)?; + let actual_len = eval(&args[1], exec_state, invoke_ctx, context)?; Err(RuntimeCheckErrorKind::TypeError( Box::new(TypeSignature::UIntType), - Box::new(TypeSignature::type_of(&actual_len)?), + Box::new(TypeSignature::type_of(actual_len.as_ref())?), ) .into()) } @@ -438,7 +465,7 @@ pub fn native_element_at(sequence: Value, index: Value) -> Result Result Result { check_argument_count(3, args)?; - let seq = eval(&args[0], env, context)?; - let left_position = eval(&args[1], env, context)?; - let right_position = eval(&args[2], env, context)?; + let seq = eval(&args[0], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?; + let left_position = eval(&args[1], exec_state, invoke_ctx, context)?; + let right_position = eval(&args[2], exec_state, invoke_ctx, context)?; let sliced_seq_res: Result = (|| { - match (seq, left_position, right_position) { + match (seq, left_position.as_ref(), right_position.as_ref()) { (Value::Sequence(seq), Value::UInt(left_position), Value::UInt(right_position)) => { - let (left_position, right_position) = - match (u32::try_from(left_position), u32::try_from(right_position)) { - (Ok(left_position), Ok(right_position)) => (left_position, right_position), - _ => return Ok(Value::none()), - }; + let (left_position, right_position) = match ( + u32::try_from(*left_position), + u32::try_from(*right_position), + ) { + (Ok(left_position), Ok(right_position)) => (left_position, right_position), + _ => return Ok(Value::none()), + }; // Perform bound checks. Not necessary to check if positions are less than 0 since the vars are unsigned. if left_position as usize >= seq.len() || right_position as usize > seq.len() { @@ -483,11 +513,14 @@ pub fn special_slice( runtime_cost( ClarityCostFunction::Slice, - env, + exec_state, (right_position - left_position) * seq.element_size()?, )?; - let seq_value = - seq.slice(env.epoch(), left_position as usize, right_position as usize)?; + let seq_value = seq.slice( + exec_state.epoch(), + left_position as usize, + right_position as usize, + )?; Ok(Value::some(seq_value)?) } _ => Err(RuntimeCheckErrorKind::Unreachable("Bad type construction".into()).into()), @@ -497,7 +530,7 @@ pub fn special_slice( match sliced_seq_res { Ok(sliced_seq) => Ok(sliced_seq), Err(e) => { - runtime_cost(ClarityCostFunction::Slice, env, 0)?; + runtime_cost(ClarityCostFunction::Slice, exec_state, 0)?; Err(e) } } @@ -505,16 +538,17 @@ pub fn special_slice( pub fn special_replace_at( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { check_argument_count(3, args)?; - let seq = eval(&args[0], env, context)?; - let seq_type = TypeSignature::type_of(&seq)?; + let seq = eval(&args[0], exec_state, invoke_ctx, context)?; + let seq_type = TypeSignature::type_of(seq.as_ref())?; // runtime is the cost to copy over one element into its place - runtime_cost(ClarityCostFunction::ReplaceAt, env, seq_type.size()?)?; + runtime_cost(ClarityCostFunction::ReplaceAt, exec_state, seq_type.size()?)?; let expected_elem_type = if let TypeSignature::SequenceType(seq_subtype) = &seq_type { seq_subtype.unit_type() @@ -523,21 +557,21 @@ pub fn special_replace_at( RuntimeCheckErrorKind::Unreachable(format!("Expected sequence: {seq_type}")).into(), ); }; - let index_val = eval(&args[1], env, context)?; - let new_element = eval(&args[2], env, context)?; + let index_val = eval(&args[1], exec_state, invoke_ctx, context)?; + let new_element = eval(&args[2], exec_state, invoke_ctx, context)?; if expected_elem_type != TypeSignature::NoType - && !expected_elem_type.admits(env.epoch(), &new_element)? + && !expected_elem_type.admits(exec_state.epoch(), new_element.as_ref())? { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(expected_elem_type), - Box::new(new_element), + new_element.as_ref().to_error_string(), ) .into()); } - let index = if let Value::UInt(index_u128) = index_val { - if let Ok(index_usize) = usize::try_from(index_u128) { + let index = if let Value::UInt(index_u128) = index_val.as_ref() { + if let Ok(index_usize) = usize::try_from(*index_u128) { index_usize } else { return Ok(Value::none()); @@ -545,12 +579,12 @@ pub fn special_replace_at( } else { return Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::UIntType), - Box::new(index_val), + index_val.as_ref().to_error_string(), ) .into()); }; - let Value::Sequence(data) = seq else { + let Value::Sequence(data) = seq.clone_with_cost(exec_state)? else { return Err( RuntimeCheckErrorKind::Unreachable(format!("Expected sequence: {seq_type}")).into(), ); @@ -559,5 +593,6 @@ pub fn special_replace_at( if index >= seq_len { return Ok(Value::none()); } - Ok(data.replace_at(env.epoch(), index, new_element)?) + let new_element = new_element.clone_with_cost(exec_state)?; + Ok(data.replace_at(exec_state.epoch(), index, new_element)?) } diff --git a/clarity/src/vm/functions/tuples.rs b/clarity/src/vm/functions/tuples.rs index 95726348f32..d3f062db07e 100644 --- a/clarity/src/vm/functions/tuples.rs +++ b/clarity/src/vm/functions/tuples.rs @@ -13,6 +13,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use crate::vm::contexts::{ExecutionState, InvocationContext}; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::runtime_cost; use crate::vm::errors::{ @@ -21,11 +22,12 @@ use crate::vm::errors::{ }; use crate::vm::representations::SymbolicExpression; use crate::vm::types::{TupleData, TypeSignature, Value}; -use crate::vm::{Environment, LocalContext, eval}; +use crate::vm::{LocalContext, eval}; pub fn tuple_cons( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { // (tuple (arg-name value) @@ -34,15 +36,22 @@ pub fn tuple_cons( check_arguments_at_least(1, args)?; - let bindings = parse_eval_bindings(args, SyntaxBindingErrorType::TupleCons, env, context)?; - runtime_cost(ClarityCostFunction::TupleCons, env, bindings.len())?; + let bindings = parse_eval_bindings( + args, + SyntaxBindingErrorType::TupleCons, + exec_state, + invoke_ctx, + context, + )?; + runtime_cost(ClarityCostFunction::TupleCons, exec_state, bindings.len())?; Ok(TupleData::from_data(bindings).map(Value::from)?) } pub fn tuple_get( args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { // (get arg-name (tuple ...)) @@ -55,14 +64,14 @@ pub fn tuple_get( "Expected name".to_string(), ))?; - let value = eval(&args[1], env, context)?; + let value = eval(&args[1], exec_state, invoke_ctx, context)?; - match value { + match value.clone_with_cost(exec_state)? { Value::Optional(opt_data) => { match opt_data.data { Some(data) => { if let Value::Tuple(tuple_data) = *data { - runtime_cost(ClarityCostFunction::TupleGet, env, tuple_data.len())?; + runtime_cost(ClarityCostFunction::TupleGet, exec_state, tuple_data.len())?; Ok(Value::some(tuple_data.get_owned(arg_name)?).map_err(|_| { VmInternalError::Expect( "Tuple contents should *always* fit in a some wrapper".into(), @@ -80,12 +89,12 @@ pub fn tuple_get( } } Value::Tuple(tuple_data) => { - runtime_cost(ClarityCostFunction::TupleGet, env, tuple_data.len())?; + runtime_cost(ClarityCostFunction::TupleGet, exec_state, tuple_data.len())?; Ok(tuple_data.get_owned(arg_name)?) } - _ => Err(RuntimeCheckErrorKind::Unreachable(format!( + other_value => Err(RuntimeCheckErrorKind::Unreachable(format!( "Expected tuple: {}", - TypeSignature::type_of(&value)? + TypeSignature::type_of(&other_value)? )) .into()), } diff --git a/clarity/src/vm/mod.rs b/clarity/src/vm/mod.rs index 4f20defe50d..287bb4542da 100644 --- a/clarity/src/vm/mod.rs +++ b/clarity/src/vm/mod.rs @@ -60,10 +60,8 @@ use self::ast::ContractAST; use self::costs::ExecutionCost; use self::diagnostic::Diagnostic; use crate::vm::callables::CallableType; -pub use crate::vm::contexts::{ - CallStack, ContractContext, Environment, LocalContext, MAX_CONTEXT_DEPTH, -}; -use crate::vm::contexts::{ExecutionTimeTracker, GlobalContext}; +pub use crate::vm::contexts::{CallStack, ContractContext, LocalContext, MAX_CONTEXT_DEPTH}; +use crate::vm::contexts::{ExecutionState, ExecutionTimeTracker, GlobalContext, InvocationContext}; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::{ CostOverflowingMath, CostTracker, LimitedCostTracker, MemoryConsumer, runtime_cost, @@ -83,6 +81,37 @@ pub use crate::vm::types::Value; use crate::vm::types::{PrincipalData, TypeSignature}; pub use crate::vm::version::ClarityVersion; +/// A wrapper for variable value references that prevents accidental cloning. +/// Only explicit clone_with_cost is allowed. Do not implement Clone or Copy for this type. +#[derive(Debug, PartialEq)] +pub enum ValueRef<'a> { + Borrowed(&'a Value), + Owned(Value), +} + +impl AsRef for ValueRef<'_> { + fn as_ref(&self) -> &Value { + match self { + ValueRef::Borrowed(r) => r, + ValueRef::Owned(o) => o, + } + } +} +impl<'a> ValueRef<'a> { + pub fn clone_with_cost( + self, + tracker: &mut T, + ) -> Result { + match self { + ValueRef::Borrowed(r) => { + runtime_cost(ClarityCostFunction::LookupVariableSize, tracker, r.size()?)?; + Ok(r.clone()) + } + ValueRef::Owned(o) => Ok(o), + } + } +} + #[derive(Debug, Clone)] pub struct ParsedContract { pub contract_identifier: String, @@ -143,75 +172,97 @@ pub trait EvalHook { // Called before the expression is evaluated fn will_begin_eval( &mut self, - _env: &mut Environment, + _env: &mut ExecutionState, + _invoke_ctx: &InvocationContext, _context: &LocalContext, _expr: &SymbolicExpression, ); // Called after the expression is evaluated - fn did_finish_eval( + fn did_finish_eval<'a>( &mut self, - _env: &mut Environment, - _context: &LocalContext, + _env: &mut ExecutionState, + _invoke_ctx: &'a InvocationContext, + _context: &'a LocalContext, _expr: &SymbolicExpression, - _res: &core::result::Result, + _res: &core::result::Result, crate::vm::errors::VmExecutionError>, ); // Called upon completion of the execution fn did_complete(&mut self, _result: core::result::Result<&mut ExecutionResult, String>); } -fn lookup_variable( +fn lookup_variable<'a>( name: &str, - context: &LocalContext, - env: &mut Environment, -) -> Result { + exec_state: &mut ExecutionState, + invoke_ctx: &'a InvocationContext, + context: &'a LocalContext, +) -> Result, VmExecutionError> { if name.starts_with(char::is_numeric) || name.starts_with('\'') { - Err( - VmInternalError::BadSymbolicRepresentation(format!("Unexpected variable name: {name}")) - .into(), - ) - } else if let Some(value) = variables::lookup_reserved_variable(name, context, env)? { - Ok(value) - } else { - runtime_cost( - ClarityCostFunction::LookupVariableDepth, - env, - context.depth(), - )?; - if let Some(value) = context.lookup_variable(name) { - runtime_cost(ClarityCostFunction::LookupVariableSize, env, value.size()?)?; - Ok(value.clone()) - } else if let Some(value) = env.contract_context.lookup_variable(name).cloned() { - runtime_cost(ClarityCostFunction::LookupVariableSize, env, value.size()?)?; - let (value, _) = - Value::sanitize_value(env.epoch(), &TypeSignature::type_of(&value)?, value) - .ok_or_else(|| RuntimeCheckErrorKind::CouldNotDetermineType)?; - Ok(value) - } else if let Some(callable_data) = context.lookup_callable_contract(name) { - if env.contract_context.get_clarity_version() < &ClarityVersion::Clarity2 { - Ok(callable_data.contract_identifier.clone().into()) - } else { - Ok(Value::CallableContract(callable_data.clone())) - } + return Err(VmInternalError::BadSymbolicRepresentation(format!( + "Unexpected variable name: {name}" + )) + .into()); + } + if let Some(value) = variables::lookup_reserved_variable(name, exec_state, invoke_ctx)? { + return Ok(ValueRef::Owned(value)); + }; + runtime_cost( + ClarityCostFunction::LookupVariableDepth, + exec_state, + context.depth(), + )?; + if let Some(value) = context.lookup_variable(name) { + let value = ValueRef::Borrowed(value); + if exec_state.epoch().uses_pre_sanitized_variables() { + // If the epoch supports value refs, we can return a borrowed reference to the variable without cloning. + return Ok(value); } else { - Err(RuntimeCheckErrorKind::Unreachable(format!("Undefined variable: {name}")).into()) + // Epochs that don't support borrowed refs must clone and pay the cost every time. + return Ok(ValueRef::Owned(value.clone_with_cost(exec_state)?)); + } + } + if let Some(value) = invoke_ctx.contract_context.lookup_variable(name) { + let value = ValueRef::Borrowed(value); + if exec_state.epoch().uses_pre_sanitized_variables() { + // Variables were sanitized at load time by canonicalize_types. + // Borrow directly. + return Ok(value); } + // Variables were not sanitized at load time, so we need to sanitize them + // now before returning and pay for the clone. + let value = value.clone_with_cost(exec_state)?; + let (value, _) = + Value::sanitize_value(exec_state.epoch(), &TypeSignature::type_of(&value)?, value) + .ok_or_else(|| RuntimeCheckErrorKind::CouldNotDetermineType)?; + return Ok(ValueRef::Owned(value)); } + if let Some(callable_data) = context.lookup_callable_contract(name) { + let value = if invoke_ctx.contract_context.get_clarity_version() < &ClarityVersion::Clarity2 + { + callable_data.contract_identifier.clone().into() + } else { + Value::CallableContract(callable_data.clone()) + }; + return Ok(ValueRef::Owned(value)); + } + Err(RuntimeCheckErrorKind::Unreachable(format!("Undefined variable: {name}")).into()) } pub fn lookup_function( name: &str, - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, ) -> Result { - runtime_cost(ClarityCostFunction::LookupFunction, env, 0)?; + runtime_cost(ClarityCostFunction::LookupFunction, exec_state, 0)?; - if let Some(result) = - functions::lookup_reserved_functions(name, env.contract_context.get_clarity_version()) - { + if let Some(result) = functions::lookup_reserved_functions( + name, + invoke_ctx.contract_context.get_clarity_version(), + ) { Ok(result) } else { - let user_function = env + let user_function = invoke_ctx .contract_context .lookup_function(name) .ok_or(RuntimeCheckErrorKind::UndefinedFunction(name.to_string()))?; @@ -219,18 +270,19 @@ pub fn lookup_function( } } -fn add_stack_trace(result: &mut Result, env: &Environment) { +fn add_stack_trace(result: &mut Result, exec_state: &mut ExecutionState) { if let Err(VmExecutionError::Runtime(_, stack_trace)) = result && stack_trace.is_none() { - stack_trace.replace(env.call_stack.make_stack_trace()); + stack_trace.replace(exec_state.call_stack.make_stack_trace()); } } pub fn apply( function: &CallableType, args: &[SymbolicExpression], - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, context: &LocalContext, ) -> Result { let identifier = function.get_identifier(); @@ -239,70 +291,74 @@ pub fn apply( // do recursion check on user functions. let track_recursion = matches!(function, CallableType::UserFunction(_)); - if track_recursion && env.call_stack.contains(&identifier) { + if track_recursion && exec_state.call_stack.contains(&identifier) { return Err(RuntimeCheckErrorKind::CircularReference(vec![identifier.to_string()]).into()); } - if env.call_stack.depth() >= max_call_stack_depth_for_epoch(*env.epoch()) { + if exec_state.call_stack.depth() >= max_call_stack_depth_for_epoch(*exec_state.epoch()) { return Err(RuntimeError::MaxStackDepthReached.into()); } if let CallableType::SpecialFunction(_, function) = function { - env.call_stack.insert(&identifier, track_recursion); - let mut resp = function(args, env, context); - add_stack_trace(&mut resp, env); - env.call_stack.remove(&identifier, track_recursion)?; + exec_state.call_stack.insert(&identifier, track_recursion); + let mut resp = function(args, exec_state, invoke_ctx, context); + add_stack_trace(&mut resp, exec_state); + exec_state.call_stack.remove(&identifier, track_recursion)?; resp } else { let mut used_memory = 0; let mut evaluated_args = Vec::with_capacity(args.len()); - env.call_stack.incr_apply_depth(); + exec_state.call_stack.incr_apply_depth(); for arg_x in args.iter() { - let arg_value = match eval(arg_x, env, context) { + let arg_value = match eval(arg_x, exec_state, invoke_ctx, context) + .and_then(|v| v.clone_with_cost(exec_state)) + { Ok(x) => x, Err(e) => { - env.drop_memory(used_memory)?; - env.call_stack.decr_apply_depth(); + exec_state.drop_memory(used_memory)?; + exec_state.call_stack.decr_apply_depth(); return Err(e); } }; let arg_use = arg_value.get_memory_use()?; - match env.add_memory(arg_use) { + match exec_state.add_memory(arg_use) { Ok(_x) => {} Err(e) => { - env.drop_memory(used_memory)?; - env.call_stack.decr_apply_depth(); + exec_state.drop_memory(used_memory)?; + exec_state.call_stack.decr_apply_depth(); return Err(VmExecutionError::from(e)); } }; - used_memory += arg_value.get_memory_use()?; + used_memory += arg_use; evaluated_args.push(arg_value); } - env.call_stack.decr_apply_depth(); + exec_state.call_stack.decr_apply_depth(); - env.call_stack.insert(&identifier, track_recursion); + exec_state.call_stack.insert(&identifier, track_recursion); let mut resp = match function { CallableType::NativeFunction(_, function, cost_function) => { - runtime_cost(cost_function.clone(), env, evaluated_args.len()) + runtime_cost(cost_function.clone(), exec_state, evaluated_args.len()) .map_err(VmExecutionError::from) - .and_then(|_| function.apply(evaluated_args, env)) + .and_then(|_| function.apply(evaluated_args, exec_state, invoke_ctx)) } CallableType::NativeFunction205(_, function, cost_function, cost_input_handle) => { - let cost_input = if env.epoch() >= &StacksEpochId::Epoch2_05 { + let cost_input = if exec_state.epoch() >= &StacksEpochId::Epoch2_05 { cost_input_handle(evaluated_args.as_slice())? } else { evaluated_args.len() as u64 }; - runtime_cost(cost_function.clone(), env, cost_input) + runtime_cost(cost_function.clone(), exec_state, cost_input) .map_err(VmExecutionError::from) - .and_then(|_| function.apply(evaluated_args, env)) + .and_then(|_| function.apply(evaluated_args, exec_state, invoke_ctx)) + } + CallableType::UserFunction(function) => { + function.apply(&evaluated_args, exec_state, invoke_ctx) } - CallableType::UserFunction(function) => function.apply(&evaluated_args, env), _ => return Err(VmInternalError::Expect("Should be unreachable.".into()).into()), }; - add_stack_trace(&mut resp, env); - env.drop_memory(used_memory)?; - env.call_stack.remove(&identifier, track_recursion)?; + add_stack_trace(&mut resp, exec_state); + exec_state.drop_memory(used_memory)?; + exec_state.call_stack.remove(&identifier, track_recursion)?; resp } } @@ -325,28 +381,29 @@ fn check_max_execution_time_expired( } } -pub fn eval( - exp: &SymbolicExpression, - env: &mut Environment, - context: &LocalContext, -) -> Result { +pub fn eval<'a>( + exp: &'a SymbolicExpression, + exec_state: &mut ExecutionState, + invoke_ctx: &'a InvocationContext, + context: &'a LocalContext, +) -> Result, VmExecutionError> { use crate::vm::representations::SymbolicExpressionType::{ Atom, AtomValue, Field, List, LiteralValue, TraitReference, }; - check_max_execution_time_expired(env.global_context)?; + check_max_execution_time_expired(exec_state.global_context)?; - if let Some(mut eval_hooks) = env.global_context.eval_hooks.take() { + if let Some(mut eval_hooks) = exec_state.global_context.eval_hooks.take() { for hook in eval_hooks.iter_mut() { - hook.will_begin_eval(env, context, exp); + hook.will_begin_eval(exec_state, invoke_ctx, context, exp); } - env.global_context.eval_hooks = Some(eval_hooks); + exec_state.global_context.eval_hooks = Some(eval_hooks); } - let res = match exp.expr { - AtomValue(ref value) | LiteralValue(ref value) => Ok(value.clone()), - Atom(ref value) => lookup_variable(value, context, env), - List(ref children) => { + let res = match &exp.expr { + AtomValue(value) | LiteralValue(value) => Ok(ValueRef::Owned(value.clone())), + Atom(value) => lookup_variable(value, exec_state, invoke_ctx, context), + List(children) => { let (function_variable, rest) = children .split_first() @@ -360,8 +417,8 @@ pub fn eval( .ok_or(RuntimeCheckErrorKind::Unreachable( "Bad function name".to_string(), ))?; - let f = lookup_function(function_name, env)?; - apply(&f, rest, env, context) + let f = lookup_function(function_name, exec_state, invoke_ctx)?; + apply(&f, rest, exec_state, invoke_ctx, context).map(ValueRef::Owned) } TraitReference(_, _) | Field(_) => { return Err(VmInternalError::BadSymbolicRepresentation( @@ -371,11 +428,11 @@ pub fn eval( } }; - if let Some(mut eval_hooks) = env.global_context.eval_hooks.take() { + if let Some(mut eval_hooks) = exec_state.global_context.eval_hooks.take() { for hook in eval_hooks.iter_mut() { - hook.did_finish_eval(env, context, exp, &res); + hook.did_finish_eval(exec_state, invoke_ctx, context, exp, &res); } - env.global_context.eval_hooks = Some(eval_hooks); + exec_state.global_context.eval_hooks = Some(eval_hooks); } res @@ -405,9 +462,17 @@ pub fn eval_all( for exp in expressions { let try_define = global_context.execute(|context| { let mut call_stack = CallStack::new(); - let mut env = Environment::new( - context, contract_context, &mut call_stack, Some(publisher.clone()), Some(publisher.clone()), sponsor.clone()); - functions::define::evaluate_define(exp, &mut env) + let mut exec_state = ExecutionState { + global_context: context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { + contract_context, + sender: Some(publisher.clone()), + caller: Some(publisher.clone()), + sponsor: sponsor.clone(), + }; + functions::define::evaluate_define(exp, &mut exec_state, &invoke_ctx) })?; match try_define { DefineResult::Variable(name, value) => { @@ -485,10 +550,17 @@ pub fn eval_all( // not a define function, evaluate normally. global_context.execute(|global_context| { let mut call_stack = CallStack::new(); - let mut env = Environment::new( - global_context, contract_context, &mut call_stack, Some(publisher.clone()), Some(publisher.clone()), sponsor.clone()); - - let result = eval(exp, &mut env, &context)?; + let mut exec_state = ExecutionState { + global_context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { + contract_context, + sender: Some(publisher.clone()), + caller: Some(publisher.clone()), + sponsor: sponsor.clone(), + }; + let result = eval(exp, &mut exec_state, &invoke_ctx, &context)?.clone_with_cost(&mut exec_state)?; last_executed = Some(result); Ok(()) })?; @@ -658,12 +730,13 @@ mod test { use super::ClarityVersion; use crate::vm::callables::{DefineType, DefinedFunction}; + use crate::vm::contexts::{ExecutionState, InvocationContext}; use crate::vm::costs::LimitedCostTracker; use crate::vm::database::MemoryBackingStore; use crate::vm::types::{QualifiedContractIdentifier, TypeSignature}; use crate::vm::{ - CallStack, ContractContext, Environment, GlobalContext, LocalContext, SymbolicExpression, - Value, eval, + CallStack, ContractContext, GlobalContext, LocalContext, SymbolicExpression, Value, + ValueRef, eval, }; #[test] @@ -717,14 +790,19 @@ mod test { .insert("do_work".into(), user_function); let mut call_stack = CallStack::new(); - let mut env = Environment::new( - &mut global_context, - &contract_context, - &mut call_stack, - None, - None, - None, + let mut exec_state = ExecutionState { + global_context: &mut global_context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { + contract_context: &contract_context, + sender: None, + caller: None, + sponsor: None, + }; + assert_eq!( + Ok(ValueRef::Owned(Value::Int(64))), + eval(&content[0], &mut exec_state, &invoke_ctx, &context) ); - assert_eq!(Ok(Value::Int(64)), eval(&content[0], &mut env, &context)); } } diff --git a/clarity/src/vm/tests/assets.rs b/clarity/src/vm/tests/assets.rs index 10ddb6afcee..dd07be07930 100644 --- a/clarity/src/vm/tests/assets.rs +++ b/clarity/src/vm/tests/assets.rs @@ -1049,9 +1049,15 @@ fn test_simple_naming_system( assert!(is_committed(&result)); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); assert_eq!( - env.eval_read_only(&names_contract_id.clone(), "(nft-get-owner? names 1)") + exec_state + .eval_read_only( + &invoke_ctx, + &names_contract_id.clone(), + "(nft-get-owner? names 1)" + ) .unwrap(), Value::some(p2.clone()).unwrap() ); @@ -1320,11 +1326,655 @@ fn test_simple_naming_system( assert!(asset_map.to_table().is_empty()); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); assert_eq!( - env.eval_read_only(&names_contract_id.clone(), "(nft-get-owner? names 5)") + exec_state + .eval_read_only( + &invoke_ctx, + &names_contract_id.clone(), + "(nft-get-owner? names 5)" + ) .unwrap(), Value::some(p1).unwrap() ); } } + +/// Contract principal constants used as principal arguments in STX operations. +#[test] +fn test_constant_contract_principal_in_stx_ops() { + let mut env_factory = env_factory(); + let mut owned_env = env_factory.get_env(StacksEpochId::Epoch34); + + let p1 = execute("'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR"); + let Value::Principal(PrincipalData::Standard(p1_std)) = p1.clone() else { + panic!("Expected standard principal data"); + }; + let Value::Principal(p1_principal) = p1.clone() else { + panic!("Expected principal data"); + }; + + let helper_id = QualifiedContractIdentifier::new(p1_std.clone(), "helper".into()); + let test_id = QualifiedContractIdentifier::new(p1_std.clone(), "test-stx".into()); + + owned_env + .initialize_versioned_contract( + helper_id.clone(), + ClarityVersion::Clarity5, + "(define-public (ping) (ok true))", + None, + ) + .unwrap(); + + let test_contract = " + (define-constant TARGET .helper) + (define-read-only (get-bal) + (stx-get-balance TARGET)) + (define-read-only (get-acct) + (stx-account TARGET)) + (define-public (do-transfer (amount uint)) + (stx-transfer? amount tx-sender TARGET)) + (define-public (do-transfer2 (amount uint)) + (stx-transfer? amount TARGET tx-sender)) + (define-public (do-transfer-memo (amount uint)) + (stx-transfer-memo? amount tx-sender TARGET 0x01020304)) + (define-public (do-burn (amount uint)) + (stx-burn? amount TARGET)) + "; + owned_env + .initialize_versioned_contract( + test_id.clone(), + ClarityVersion::Clarity5, + test_contract, + None, + ) + .unwrap(); + + // stx-get-balance with constant contract principal + let (result, _, _) = execute_transaction( + &mut owned_env, + p1_principal.clone(), + &test_id, + "get-bal", + &[], + ) + .unwrap(); + assert_eq!(result, Value::UInt(0)); + + // stx-account with constant contract principal + let (result, _, _) = execute_transaction( + &mut owned_env, + p1_principal.clone(), + &test_id, + "get-acct", + &[], + ) + .unwrap(); + // stx-account returns a tuple; just verify it succeeds + assert!(matches!(result, Value::Tuple(_))); + + // Fund p1 so transfers work + owned_env.stx_faucet(&p1_principal, 10_000); + + // stx-transfer? with constant contract principal as recipient + let (result, _, _) = execute_transaction( + &mut owned_env, + p1_principal.clone(), + &test_id, + "do-transfer", + &symbols_from_values(vec![Value::UInt(100)]), + ) + .unwrap(); + assert!(is_committed(&result)); + + // stx-transfer? with constant contract principal as sender + let (result, _, _) = execute_transaction( + &mut owned_env, + p1_principal.clone(), + &test_id, + "do-transfer2", + &symbols_from_values(vec![Value::UInt(100)]), + ) + .unwrap(); + // This should fail, but only because a send from sender != tx-sender fails + let expected = Value::err_uint(4); + assert_eq!(result, expected); + + // stx-transfer-memo? with constant contract principal as recipient + let (result, _, _) = execute_transaction( + &mut owned_env, + p1_principal.clone(), + &test_id, + "do-transfer-memo", + &symbols_from_values(vec![Value::UInt(100)]), + ) + .unwrap(); + assert!(is_committed(&result)); + + // stx-burn? with constant contract principal as sender arg. + // This will fail with SENDER_IS_NOT_TX_SENDER (the caller is p1, not + // the helper contract), but the important thing is it doesn't crash + // with TypeValueError from failing to match Value::Principal. + let (result, _, _) = execute_transaction( + &mut owned_env, + p1_principal, + &test_id, + "do-burn", + &symbols_from_values(vec![Value::UInt(100)]), + ) + .unwrap(); + assert!(is_err_code(&result, 4)); +} + +/// Contract principal constants used as principal arguments in FT operations. +#[test] +fn test_constant_contract_principal_in_ft_ops() { + let mut env_factory = env_factory(); + let mut owned_env = env_factory.get_env(StacksEpochId::Epoch34); + + let p1 = execute("'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR"); + let Value::Principal(PrincipalData::Standard(p1_std)) = p1.clone() else { + panic!("Expected standard principal data"); + }; + let Value::Principal(p1_principal) = p1.clone() else { + panic!("Expected principal data"); + }; + + let helper_id = QualifiedContractIdentifier::new(p1_std.clone(), "helper".into()); + let test_id = QualifiedContractIdentifier::new(p1_std.clone(), "test-ft".into()); + + owned_env + .initialize_versioned_contract( + helper_id, + ClarityVersion::Clarity5, + "(define-public (ping) (ok true))", + None, + ) + .unwrap(); + + let test_contract = " + (define-fungible-token my-ft) + (define-constant TARGET .helper) + (define-public (do-mint (amount uint)) + (ft-mint? my-ft amount TARGET)) + (define-read-only (get-bal) + (ft-get-balance my-ft TARGET)) + (define-public (do-transfer (amount uint) (to principal)) + (ft-transfer? my-ft amount TARGET to)) + (define-public (do-burn (amount uint)) + (ft-burn? my-ft amount TARGET)) + "; + owned_env + .initialize_versioned_contract( + test_id.clone(), + ClarityVersion::Clarity5, + test_contract, + None, + ) + .unwrap(); + + // ft-mint? with constant as recipient + let (result, _, _) = execute_transaction( + &mut owned_env, + p1_principal.clone(), + &test_id, + "do-mint", + &symbols_from_values(vec![Value::UInt(500)]), + ) + .unwrap(); + assert!(is_committed(&result)); + + // ft-get-balance with constant + let (result, _, _) = execute_transaction( + &mut owned_env, + p1_principal.clone(), + &test_id, + "get-bal", + &[], + ) + .unwrap(); + assert_eq!(result, Value::UInt(500)); + + // ft-transfer? with constant as sender + let (result, _, _) = execute_transaction( + &mut owned_env, + p1_principal.clone(), + &test_id, + "do-transfer", + &symbols_from_values(vec![Value::UInt(100), p1.clone()]), + ) + .unwrap(); + assert!(is_committed(&result)); + + // ft-burn? with constant as owner + let (result, _, _) = execute_transaction( + &mut owned_env, + p1_principal, + &test_id, + "do-burn", + &symbols_from_values(vec![Value::UInt(50)]), + ) + .unwrap(); + assert!(is_committed(&result)); +} + +/// Contract principal constants used as principal arguments in NFT +/// mint/transfer/burn operations (the to/from/sender args, not the token ID +/// which is covered below in +/// `test_nft_with_constant_contract_principal_as_token_id`). +#[test] +fn test_constant_contract_principal_in_nft_principal_args() { + let mut env_factory = env_factory(); + let mut owned_env = env_factory.get_env(StacksEpochId::Epoch34); + + let p1 = execute("'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR"); + let Value::Principal(PrincipalData::Standard(p1_std)) = p1.clone() else { + panic!("Expected standard principal data"); + }; + let Value::Principal(p1_principal) = p1.clone() else { + panic!("Expected principal data"); + }; + + let helper_id = QualifiedContractIdentifier::new(p1_std.clone(), "helper".into()); + let test_id = QualifiedContractIdentifier::new(p1_std.clone(), "test-nft".into()); + + owned_env + .initialize_versioned_contract( + helper_id, + ClarityVersion::Clarity5, + "(define-public (ping) (ok true))", + None, + ) + .unwrap(); + + let test_contract = " + (define-non-fungible-token my-nft uint) + (define-constant TARGET .helper) + (define-public (do-mint (id uint)) + (nft-mint? my-nft id TARGET)) + (define-public (do-transfer (id uint) (to principal)) + (nft-transfer? my-nft id TARGET to)) + (define-public (do-burn (id uint)) + (nft-burn? my-nft id TARGET)) + "; + owned_env + .initialize_versioned_contract( + test_id.clone(), + ClarityVersion::Clarity5, + test_contract, + None, + ) + .unwrap(); + + // nft-mint? with constant as recipient + let (result, _, _) = execute_transaction( + &mut owned_env, + p1_principal.clone(), + &test_id, + "do-mint", + &symbols_from_values(vec![Value::UInt(1)]), + ) + .unwrap(); + assert!(is_committed(&result)); + + // nft-transfer? with constant as sender + let (result, _, _) = execute_transaction( + &mut owned_env, + p1_principal.clone(), + &test_id, + "do-transfer", + &symbols_from_values(vec![Value::UInt(1), p1]), + ) + .unwrap(); + assert!(is_committed(&result)); + + // Mint another for burn test + let (result, _, _) = execute_transaction( + &mut owned_env, + p1_principal.clone(), + &test_id, + "do-mint", + &symbols_from_values(vec![Value::UInt(2)]), + ) + .unwrap(); + assert!(is_committed(&result)); + + // nft-burn? with constant as owner + let (result, _, _) = execute_transaction( + &mut owned_env, + p1_principal, + &test_id, + "do-burn", + &symbols_from_values(vec![Value::UInt(2)]), + ) + .unwrap(); + assert!(is_committed(&result)); +} + +/// Contract principal constants wrapped in compound values (optional, response, +/// tuple, list) and then passed to native functions that expect principals. +/// Guards against regressions if the value representation changes again. +#[test] +fn test_constant_contract_principal_in_compound_values() { + let mut env_factory = env_factory(); + let mut owned_env = env_factory.get_env(StacksEpochId::Epoch34); + + let p1 = execute("'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR"); + let Value::Principal(PrincipalData::Standard(p1_std)) = p1.clone() else { + panic!("Expected standard principal data"); + }; + let Value::Principal(p1_principal) = p1.clone() else { + panic!("Expected principal data"); + }; + + let helper_id = QualifiedContractIdentifier::new(p1_std.clone(), "helper".into()); + let test_id = QualifiedContractIdentifier::new(p1_std.clone(), "test-compound".into()); + + owned_env + .initialize_versioned_contract( + helper_id, + ClarityVersion::Clarity5, + "(define-public (ping) (ok true))", + None, + ) + .unwrap(); + + let test_contract = " + (define-constant TARGET .helper) + + ;; Wrap constant in an optional and unwrap to use as principal + (define-read-only (via-optional) + (stx-get-balance (unwrap-panic (some TARGET)))) + + ;; Wrap constant in an ok response and unwrap to use as principal + (define-read-only (via-response) + (stx-get-balance (unwrap-panic (ok TARGET)))) + + ;; Wrap constant in a tuple and extract to use as principal + (define-read-only (via-tuple) + (stx-get-balance (get addr { addr: TARGET }))) + + ;; Put constant in a list and extract to use as principal + (define-read-only (via-list) + (stx-get-balance (unwrap-panic (element-at? (list TARGET) u0)))) + "; + owned_env + .initialize_versioned_contract( + test_id.clone(), + ClarityVersion::Clarity5, + test_contract, + None, + ) + .unwrap(); + + for func in &["via-optional", "via-response", "via-tuple", "via-list"] { + let (result, _, _) = + execute_transaction(&mut owned_env, p1_principal.clone(), &test_id, func, &[]).unwrap(); + assert_eq!(result, Value::UInt(0), "{func} failed"); + } +} + +/// Verify that NFT operations work correctly when a contract principal +/// constant is used as the token identifier, and that the asset map and +/// events contain the canonical `Value::Principal` form. +#[test] +fn test_nft_with_constant_contract_principal_as_token_id() { + let mut env_factory = env_factory(); + let mut owned_env = env_factory.get_env(StacksEpochId::Epoch34); + + let p1 = execute("'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR"); + let p2 = execute("'SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G"); + + let Value::Principal(PrincipalData::Standard(p1_std)) = p1.clone() else { + panic!("Expected standard principal data"); + }; + let Value::Principal(p1_principal) = p1.clone() else { + panic!("Expected principal data"); + }; + + let helper_contract_id = QualifiedContractIdentifier::new(p1_std.clone(), "helper".into()); + let nft_contract_id = QualifiedContractIdentifier::new(p1_std.clone(), "nft-contract".into()); + + // A trivial contract so we have a valid contract principal to reference. + owned_env + .initialize_versioned_contract( + helper_contract_id.clone(), + ClarityVersion::Clarity5, + "(define-public (ping) (ok true))", + None, + ) + .unwrap(); + + let nft_contract = " + (define-non-fungible-token nft principal) + (define-constant CALLABLE_ID .helper) + (define-public (mint) + (nft-mint? nft CALLABLE_ID tx-sender)) + (define-public (xfer (to principal)) + (nft-transfer? nft CALLABLE_ID tx-sender to)) + "; + owned_env + .initialize_versioned_contract( + nft_contract_id.clone(), + ClarityVersion::Clarity5, + nft_contract, + None, + ) + .unwrap(); + + // Mint the NFT using the constant as token id. + let (result, _asset_map, _events) = execute_transaction( + &mut owned_env, + p1_principal.clone(), + &nft_contract_id, + "mint", + &[], + ) + .unwrap(); + assert!(is_committed(&result)); + + // Transfer the NFT – the constant flows through log_asset_transfer + // into the asset map. + let (result, asset_map, events) = execute_transaction( + &mut owned_env, + p1_principal.clone(), + &nft_contract_id, + "xfer", + &symbols_from_values(vec![p2]), + ) + .unwrap(); + assert!(is_committed(&result)); + + let table = asset_map.to_table(); + let p1_assets = table + .get(&p1_principal) + .expect("p1 should have asset entries"); + let nft_identifier = AssetIdentifier { + contract_identifier: nft_contract_id, + asset_name: "nft".into(), + }; + let entry = p1_assets + .get(&nft_identifier) + .expect("should have an NFT entry"); + + match entry { + AssetMapEntry::Asset(values) => { + assert_eq!(values.len(), 1); + let expected = Value::Principal(PrincipalData::Contract(helper_contract_id)); + assert_eq!( + values[0], expected, + "asset map value must equal the contract principal" + ); + } + other => panic!("expected AssetMapEntry::Asset, got: {:?}", other), + } + + // NFT events must contain Value::Principal. + let nft_transfer_event = events.iter().find(|e| { + matches!( + e, + StacksTransactionEvent::NFTEvent(crate::vm::events::NFTEventType::NFTTransferEvent(_)) + ) + }); + if let Some(StacksTransactionEvent::NFTEvent( + crate::vm::events::NFTEventType::NFTTransferEvent(data), + )) = nft_transfer_event + { + assert!( + matches!(&data.value, Value::Principal(PrincipalData::Contract(_))), + "NFT transfer event must contain Value::Principal, got: {:?}", + data.value + ); + } else { + panic!("expected an NFT transfer event"); + } +} + +/// Verify that `is-eq` returns true when comparing a contract principal +/// constant against the same principal passed as an argument or written +/// as a literal. +#[test] +fn test_constant_contract_principal_is_eq() { + let mut env_factory = env_factory(); + let mut owned_env = env_factory.get_env(StacksEpochId::Epoch34); + + let p1 = execute("'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR"); + let Value::Principal(PrincipalData::Standard(p1_std)) = p1.clone() else { + panic!("Expected standard principal data"); + }; + let Value::Principal(p1_principal) = p1.clone() else { + panic!("Expected principal data"); + }; + + let helper_id = QualifiedContractIdentifier::new(p1_std.clone(), "helper".into()); + let test_id = QualifiedContractIdentifier::new(p1_std.clone(), "test-contract".into()); + + owned_env + .initialize_versioned_contract( + helper_id, + ClarityVersion::Clarity5, + "(define-public (ping) (ok true))", + None, + ) + .unwrap(); + + let test_contract = " + (define-constant CALLABLE_ID .helper) + (define-read-only (check-eq (p principal)) + (is-eq CALLABLE_ID p)) + (define-read-only (check-eq-literal) + (is-eq CALLABLE_ID 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR.helper)) + "; + owned_env + .initialize_versioned_contract( + test_id.clone(), + ClarityVersion::Clarity5, + test_contract, + None, + ) + .unwrap(); + + let helper_principal = execute("'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR.helper"); + let (result, _asset_map, _events) = execute_transaction( + &mut owned_env, + p1_principal.clone(), + &test_id, + "check-eq", + &symbols_from_values(vec![helper_principal]), + ) + .unwrap(); + + assert_eq!(result, Value::Bool(true)); + + let (result_literal, _asset_map, _events) = execute_transaction( + &mut owned_env, + p1_principal, + &test_id, + "check-eq-literal", + &[], + ) + .unwrap(); + + assert_eq!(result_literal, Value::Bool(true)); +} + +/// Verify that `index-of?` and list equality work correctly when a contract +/// principal constant is compared against principal values in a list. +#[test] +fn test_constant_contract_principal_index_of_and_list_ops() { + let mut env_factory = env_factory(); + let mut owned_env = env_factory.get_env(StacksEpochId::Epoch34); + + let p1 = execute("'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR"); + let Value::Principal(PrincipalData::Standard(p1_std)) = p1.clone() else { + panic!("Expected standard principal data"); + }; + let Value::Principal(p1_principal) = p1 else { + panic!("Expected principal data"); + }; + + let helper_id = QualifiedContractIdentifier::new(p1_std.clone(), "helper".into()); + let test_id = QualifiedContractIdentifier::new(p1_std.clone(), "test-contract".into()); + + owned_env + .initialize_versioned_contract( + helper_id, + ClarityVersion::Clarity5, + "(define-public (ping) (ok true))", + None, + ) + .unwrap(); + + let test_contract = " + (define-constant CALLABLE_ID .helper) + + ;; Search for constant in a list of principal literals + (define-read-only (index-of-callable-in-principal-list) + (index-of? (list 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR.helper) CALLABLE_ID)) + + ;; Search for principal literal in a list built with the constant + (define-read-only (index-of-principal-in-callable-list) + (index-of? (list CALLABLE_ID) 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR.helper)) + + ;; Compare a list containing the constant against + ;; a list containing the equivalent principal literal + (define-read-only (list-eq) + (is-eq (list CALLABLE_ID) (list 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR.helper))) + "; + owned_env + .initialize_versioned_contract( + test_id.clone(), + ClarityVersion::Clarity5, + test_contract, + None, + ) + .unwrap(); + + // index-of? should find the constant in a principal list + let (result, _, _) = execute_transaction( + &mut owned_env, + p1_principal.clone(), + &test_id, + "index-of-callable-in-principal-list", + &[], + ) + .unwrap(); + assert_eq!(result, Value::some(Value::UInt(0)).unwrap()); + + // index-of? should find a principal in a list built from the constant + let (result, _, _) = execute_transaction( + &mut owned_env, + p1_principal.clone(), + &test_id, + "index-of-principal-in-callable-list", + &[], + ) + .unwrap(); + assert_eq!(result, Value::some(Value::UInt(0)).unwrap()); + + // Lists containing constant/literal principal values should be equal + let (result, _, _) = + execute_transaction(&mut owned_env, p1_principal, &test_id, "list-eq", &[]).unwrap(); + assert_eq!(result, Value::Bool(true)); +} diff --git a/clarity/src/vm/tests/contracts.rs b/clarity/src/vm/tests/contracts.rs index 019594413be..b65dde8923e 100644 --- a/clarity/src/vm/tests/contracts.rs +++ b/clarity/src/vm/tests/contracts.rs @@ -20,7 +20,7 @@ use stacks_common::types::{StacksEpochId, chainstate::BlockHeaderHash}; #[cfg(test)] use stacks_common::util::hash::Sha512Trunc256Sum; -use crate::vm::contexts::Environment; +use crate::vm::contexts::{ExecutionState, InvocationContext}; use crate::vm::tests::{test_clarity_versions, test_epochs}; use crate::vm::types::{PrincipalData, QualifiedContractIdentifier, StandardPrincipalData, Value}; #[cfg(test)] @@ -115,12 +115,12 @@ fn test_get_block_info_eval( Ok(Value::none()), Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::UIntType), - Box::new(Value::Int(-1)), + Value::Int(-1).to_error_string(), ) .into()), Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::UIntType), - Box::new(Value::Bool(true)), + Value::Bool(true).to_error_string(), ) .into()), Ok(Value::none()), @@ -146,9 +146,11 @@ fn test_get_block_info_eval( ) .unwrap(); - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); eprintln!("{}", contracts[i]); - let eval_result = env.eval_read_only(&contract_identifier, "(test-func)"); + let eval_result = + exec_state.eval_read_only(&invoke_ctx, &contract_identifier, "(test-func)"); match expected[i] { // any (some UINT) is okay for checking get-block-info? time Ok(Value::UInt(0)) => { @@ -186,114 +188,139 @@ fn test_contract_caller(epoch: StacksEpochId, mut env_factory: MemoryEnvironment ); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("contract-a").unwrap(), - contract_a, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("contract-b").unwrap(), - contract_b, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-a").unwrap(), + contract_a, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-b").unwrap(), + contract_b, + ) + .unwrap(); } { let c_b = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("contract-b").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.clone().expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("contract-a").unwrap(), - "get-caller", - &[], - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("contract-a").unwrap(), + "get-caller", + &[], + false + ) + .unwrap(), Value::cons_list_unsanitized(vec![p1.clone(), p1.clone()]).unwrap() ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("contract-b").unwrap(), - "as-contract-get-caller", - &[], - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("contract-b").unwrap(), + "as-contract-get-caller", + &[], + false + ) + .unwrap(), Value::cons_list_unsanitized(vec![c_b.clone(), c_b.clone()]).unwrap() ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("contract-b").unwrap(), - "cc-get-caller", - &[], - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("contract-b").unwrap(), + "cc-get-caller", + &[], + false + ) + .unwrap(), Value::cons_list_unsanitized(vec![c_b.clone(), p1]).unwrap() ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("contract-b").unwrap(), - "as-contract-cc-get-caller", - &[], - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("contract-b").unwrap(), + "as-contract-cc-get-caller", + &[], + false + ) + .unwrap(), Value::cons_list_unsanitized(vec![c_b.clone(), c_b]).unwrap() ); } } -fn tx_sponsor_contract_asserts(env: &mut Environment, sponsor: Option) { +fn tx_sponsor_contract_asserts( + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, + sponsor: Option, +) { let sponsor = match sponsor { None => Value::none(), Some(p) => Value::some(Value::Principal(p)).unwrap(), }; assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("contract-a").unwrap(), - "get-sponsor", - &[], - false - ) - .unwrap(), + exec_state + .execute_contract( + invoke_ctx, + &QualifiedContractIdentifier::local("contract-a").unwrap(), + "get-sponsor", + &[], + false + ) + .unwrap(), Value::cons_list_unsanitized(vec![sponsor.clone()]).unwrap() ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("contract-b").unwrap(), - "as-contract-get-sponsor", - &[], - false - ) - .unwrap(), + exec_state + .execute_contract( + invoke_ctx, + &QualifiedContractIdentifier::local("contract-b").unwrap(), + "as-contract-get-sponsor", + &[], + false + ) + .unwrap(), Value::cons_list_unsanitized(vec![sponsor.clone()]).unwrap() ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("contract-b").unwrap(), - "cc-get-sponsor", - &[], - false - ) - .unwrap(), + exec_state + .execute_contract( + invoke_ctx, + &QualifiedContractIdentifier::local("contract-b").unwrap(), + "cc-get-sponsor", + &[], + false + ) + .unwrap(), Value::cons_list_unsanitized(vec![sponsor.clone()]).unwrap() ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("contract-b").unwrap(), - "as-contract-cc-get-sponsor", - &[], - false - ) - .unwrap(), + exec_state + .execute_contract( + invoke_ctx, + &QualifiedContractIdentifier::local("contract-b").unwrap(), + "as-contract-cc-get-sponsor", + &[], + false + ) + .unwrap(), Value::cons_list_unsanitized(vec![sponsor]).unwrap() ); } @@ -330,33 +357,37 @@ fn test_tx_sponsor(epoch: StacksEpochId, mut env_factory: MemoryEnvironmentGener }; { - let mut env = + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment(Some(p1.clone()), sponsor.clone(), &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("contract-a").unwrap(), - contract_a, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("contract-b").unwrap(), - contract_b, - ) - .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-a").unwrap(), + contract_a, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-b").unwrap(), + contract_b, + ) + .unwrap(); } // Sponsor is equal to some(principal) in this code block. { - let mut env = + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment(Some(p1.clone()), sponsor.clone(), &placeholder_context); - tx_sponsor_contract_asserts(&mut env, sponsor); + tx_sponsor_contract_asserts(&mut exec_state, &invoke_ctx, sponsor); } // Sponsor is none in this code block. { let sponsor = None; - let mut env = + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment(Some(p1), sponsor.clone(), &placeholder_context); - tx_sponsor_contract_asserts(&mut env, sponsor); + tx_sponsor_contract_asserts(&mut exec_state, &invoke_ctx, sponsor); } } @@ -385,66 +416,79 @@ fn test_fully_qualified_contract_call( ); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("contract-a").unwrap(), - contract_a, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("contract-b").unwrap(), - contract_b, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-a").unwrap(), + contract_a, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-b").unwrap(), + contract_b, + ) + .unwrap(); } { let c_b = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("contract-b").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.clone().expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("contract-a").unwrap(), - "get-caller", - &[], - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("contract-a").unwrap(), + "get-caller", + &[], + false + ) + .unwrap(), Value::cons_list_unsanitized(vec![p1.clone(), p1.clone()]).unwrap() ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("contract-b").unwrap(), - "as-contract-get-caller", - &[], - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("contract-b").unwrap(), + "as-contract-get-caller", + &[], + false + ) + .unwrap(), Value::cons_list_unsanitized(vec![c_b.clone(), c_b.clone()]).unwrap() ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("contract-b").unwrap(), - "cc-get-caller", - &[], - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("contract-b").unwrap(), + "cc-get-caller", + &[], + false + ) + .unwrap(), Value::cons_list_unsanitized(vec![c_b.clone(), p1]).unwrap() ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("contract-b").unwrap(), - "as-contract-cc-get-caller", - &[], - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("contract-b").unwrap(), + "as-contract-cc-get-caller", + &[], + false + ) + .unwrap(), Value::cons_list_unsanitized(vec![c_b.clone(), c_b]).unwrap() ); } @@ -522,156 +566,179 @@ fn test_simple_naming_system(epoch: StacksEpochId, mut env_factory: MemoryEnviro ); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); let contract_identifier = QualifiedContractIdentifier::local("tokens").unwrap(); - env.initialize_contract(contract_identifier, tokens_contract) + exec_state + .initialize_contract(&invoke_ctx, contract_identifier, tokens_contract) .unwrap(); let contract_identifier = QualifiedContractIdentifier::local("names").unwrap(); - env.initialize_contract(contract_identifier, names_contract) + exec_state + .initialize_contract(&invoke_ctx, contract_identifier, names_contract) .unwrap(); } { - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p2.clone().expect_principal().unwrap()), None, &placeholder_context, ); assert!(is_err_code( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "preorder", - &symbols_from_values(vec![name_hash_expensive_0.clone(), Value::UInt(1000)]), - false - ) - .unwrap(), + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "preorder", + &symbols_from_values(vec![name_hash_expensive_0.clone(), Value::UInt(1000)]), + false + ) + .unwrap(), 1 )); } { - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.clone().expect_principal().unwrap()), None, &placeholder_context, ); assert!(is_committed( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "preorder", - &symbols_from_values(vec![name_hash_expensive_0.clone(), Value::UInt(1000)]), - false - ) - .unwrap() + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "preorder", + &symbols_from_values(vec![name_hash_expensive_0.clone(), Value::UInt(1000)]), + false + ) + .unwrap() )); assert!(is_err_code( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "preorder", - &symbols_from_values(vec![name_hash_expensive_0, Value::UInt(1000)]), - false - ) - .unwrap(), + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "preorder", + &symbols_from_values(vec![name_hash_expensive_0, Value::UInt(1000)]), + false + ) + .unwrap(), 2 )); } { // shouldn't be able to register a name you didn't preorder! - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p2.clone().expect_principal().unwrap()), None, &placeholder_context, ); assert!(is_err_code( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "register", - &symbols_from_values(vec![p2.clone(), Value::Int(1), Value::Int(0)]), - false - ) - .unwrap(), + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "register", + &symbols_from_values(vec![p2.clone(), Value::Int(1), Value::Int(0)]), + false + ) + .unwrap(), 4 )); } { // should work! - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert!(is_committed( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "register", - &symbols_from_values(vec![p2.clone(), Value::Int(1), Value::Int(0)]), - false - ) - .unwrap() + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "register", + &symbols_from_values(vec![p2.clone(), Value::Int(1), Value::Int(0)]), + false + ) + .unwrap() )); } { // try to underpay! - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p2.clone().expect_principal().unwrap()), None, &placeholder_context, ); assert!(is_committed( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "preorder", - &symbols_from_values(vec![name_hash_expensive_1, Value::UInt(100)]), - false - ) - .unwrap() + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "preorder", + &symbols_from_values(vec![name_hash_expensive_1, Value::UInt(100)]), + false + ) + .unwrap() )); assert!(is_err_code( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "register", - &symbols_from_values(vec![p2.clone(), Value::Int(2), Value::Int(0)]), - false - ) - .unwrap(), + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "register", + &symbols_from_values(vec![p2.clone(), Value::Int(2), Value::Int(0)]), + false + ) + .unwrap(), 4 )); // register a cheap name! assert!(is_committed( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "preorder", - &symbols_from_values(vec![name_hash_cheap_0, Value::UInt(100)]), - false - ) - .unwrap() + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "preorder", + &symbols_from_values(vec![name_hash_cheap_0, Value::UInt(100)]), + false + ) + .unwrap() )); assert!(is_committed( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "register", - &symbols_from_values(vec![p2.clone(), Value::Int(100001), Value::Int(0)]), - false - ) - .unwrap() + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "register", + &symbols_from_values(vec![p2.clone(), Value::Int(100001), Value::Int(0)]), + false + ) + .unwrap() )); // preorder must exist! assert!(is_err_code( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "register", - &symbols_from_values(vec![p2, Value::Int(100001), Value::Int(0)]), - false - ) - .unwrap(), + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "register", + &symbols_from_values(vec![p2, Value::Int(100001), Value::Int(0)]), + false + ) + .unwrap(), 5 )); } @@ -691,18 +758,20 @@ fn test_simple_contract_call(epoch: StacksEpochId, mut env_factory: MemoryEnviro ClarityVersion::Clarity2, ); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(get_principal().expect_principal().unwrap()), None, &placeholder_context, ); let contract_identifier = QualifiedContractIdentifier::local("factorial-contract").unwrap(); - env.initialize_contract(contract_identifier, contract_1) + exec_state + .initialize_contract(&invoke_ctx, contract_identifier, contract_1) .unwrap(); let contract_identifier = QualifiedContractIdentifier::local("proxy-compute").unwrap(); - env.initialize_contract(contract_identifier, contract_2) + exec_state + .initialize_contract(&invoke_ctx, contract_identifier, contract_2) .unwrap(); let args = symbols_from_values(vec![]); @@ -716,19 +785,23 @@ fn test_simple_contract_call(epoch: StacksEpochId, mut env_factory: MemoryEnviro Value::Int(120), ]; for expected_result in &expected { - env.execute_contract( - &QualifiedContractIdentifier::local("proxy-compute").unwrap(), - "proxy-compute", - &args, - false, - ) - .unwrap(); - assert_eq!( - env.eval_read_only( - &QualifiedContractIdentifier::local("factorial-contract").unwrap(), - "(get current (unwrap! (map-get? factorials {id: 8008}) false))" + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("proxy-compute").unwrap(), + "proxy-compute", + &args, + false, ) - .unwrap(), + .unwrap(); + assert_eq!( + exec_state + .eval_read_only( + &invoke_ctx, + &QualifiedContractIdentifier::local("factorial-contract").unwrap(), + "(get current (unwrap! (map-get? factorials {id: 8008}) false))" + ) + .unwrap(), *expected_result ); } @@ -777,26 +850,31 @@ fn test_aborts(epoch: StacksEpochId, mut env_factory: MemoryEnvironmentGenerator ClarityVersion::Clarity2, ); - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, mut invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); let contract_identifier = QualifiedContractIdentifier::local("contract-1").unwrap(); - env.initialize_contract(contract_identifier, contract_1) + exec_state + .initialize_contract(&invoke_ctx, contract_identifier, contract_1) .unwrap(); let contract_identifier = QualifiedContractIdentifier::local("contract-2").unwrap(); - env.initialize_contract(contract_identifier, contract_2) + exec_state + .initialize_contract(&invoke_ctx, contract_identifier, contract_2) .unwrap(); - env.sender = Some(get_principal_as_principal_data()); + invoke_ctx.sender = Some(get_principal_as_principal_data()); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("contract-1").unwrap(), - "modify-data", - &symbols_from_values(vec![Value::Int(10), Value::Int(10)]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("contract-1").unwrap(), + "modify-data", + &symbols_from_values(vec![Value::Int(10), Value::Int(10)]), + false + ) + .unwrap(), Value::Response(ResponseData { committed: true, data: Box::new(Value::Int(1)) @@ -804,13 +882,15 @@ fn test_aborts(epoch: StacksEpochId, mut env_factory: MemoryEnvironmentGenerator ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("contract-1").unwrap(), - "modify-data", - &symbols_from_values(vec![Value::Int(20), Value::Int(10)]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("contract-1").unwrap(), + "modify-data", + &symbols_from_values(vec![Value::Int(20), Value::Int(10)]), + false + ) + .unwrap(), Value::Response(ResponseData { committed: false, data: Box::new(Value::Int(1)) @@ -818,31 +898,37 @@ fn test_aborts(epoch: StacksEpochId, mut env_factory: MemoryEnvironmentGenerator ); assert_eq!( - env.eval_read_only( - &QualifiedContractIdentifier::local("contract-1").unwrap(), - "(get-data 20)" - ) - .unwrap(), + exec_state + .eval_read_only( + &invoke_ctx, + &QualifiedContractIdentifier::local("contract-1").unwrap(), + "(get-data 20)" + ) + .unwrap(), Value::Int(0) ); assert_eq!( - env.eval_read_only( - &QualifiedContractIdentifier::local("contract-1").unwrap(), - "(get-data 10)" - ) - .unwrap(), + exec_state + .eval_read_only( + &invoke_ctx, + &QualifiedContractIdentifier::local("contract-1").unwrap(), + "(get-data 10)" + ) + .unwrap(), Value::Int(10) ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("contract-2").unwrap(), - "fail-in-other", - &symbols_from_values(vec![]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("contract-2").unwrap(), + "fail-in-other", + &symbols_from_values(vec![]), + false + ) + .unwrap(), Value::Response(ResponseData { committed: true, data: Box::new(Value::Int(1)) @@ -850,13 +936,15 @@ fn test_aborts(epoch: StacksEpochId, mut env_factory: MemoryEnvironmentGenerator ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("contract-2").unwrap(), - "fail-in-self", - &symbols_from_values(vec![]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("contract-2").unwrap(), + "fail-in-self", + &symbols_from_values(vec![]), + false + ) + .unwrap(), Value::Response(ResponseData { committed: false, data: Box::new(Value::Int(1)) @@ -864,20 +952,24 @@ fn test_aborts(epoch: StacksEpochId, mut env_factory: MemoryEnvironmentGenerator ); assert_eq!( - env.eval_read_only( - &QualifiedContractIdentifier::local("contract-1").unwrap(), - "(get-data 105)" - ) - .unwrap(), + exec_state + .eval_read_only( + &invoke_ctx, + &QualifiedContractIdentifier::local("contract-1").unwrap(), + "(get-data 105)" + ) + .unwrap(), Value::Int(0) ); assert_eq!( - env.eval_read_only( - &QualifiedContractIdentifier::local("contract-1").unwrap(), - "(get-data 100)" - ) - .unwrap(), + exec_state + .eval_read_only( + &invoke_ctx, + &QualifiedContractIdentifier::local("contract-1").unwrap(), + "(get-data 100)" + ) + .unwrap(), Value::Int(0) ); } @@ -891,10 +983,12 @@ fn test_factorial_contract(epoch: StacksEpochId, mut env_factory: MemoryEnvironm ClarityVersion::Clarity2, ); - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, mut invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); let contract_identifier = QualifiedContractIdentifier::local("factorial").unwrap(); - env.initialize_contract(contract_identifier, FACTORIAL_CONTRACT) + exec_state + .initialize_contract(&invoke_ctx, contract_identifier, FACTORIAL_CONTRACT) .unwrap(); let tx_name = "compute"; @@ -926,32 +1020,37 @@ fn test_factorial_contract(epoch: StacksEpochId, mut env_factory: MemoryEnvironm Value::Int(120), ]; - env.sender = Some(get_principal_as_principal_data()); + invoke_ctx.sender = Some(get_principal_as_principal_data()); for (arguments, expectation) in arguments_to_test.iter().zip(expected.iter()) { - env.execute_contract( - &QualifiedContractIdentifier::local("factorial").unwrap(), - tx_name, - arguments, - false, - ) - .unwrap(); + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("factorial").unwrap(), + tx_name, + arguments, + false, + ) + .unwrap(); assert_eq!( *expectation, - env.eval_read_only( - &QualifiedContractIdentifier::local("factorial").unwrap(), - &format!( - "(unwrap! (get current (map-get? factorials (tuple (id {})))) false)", - arguments[0] + exec_state + .eval_read_only( + &invoke_ctx, + &QualifiedContractIdentifier::local("factorial").unwrap(), + &format!( + "(unwrap! (get current (map-get? factorials (tuple (id {})))) false)", + arguments[0] + ) ) - ) - .unwrap() + .unwrap() ); } - let err_result = env + let err_result = exec_state .execute_contract( + &invoke_ctx, &QualifiedContractIdentifier::local("factorial").unwrap(), "init-factorial", &symbols_from_values(vec![Value::Int(9000), Value::Int(15)]), @@ -963,8 +1062,9 @@ fn test_factorial_contract(epoch: StacksEpochId, mut env_factory: MemoryEnvironm VmExecutionError::RuntimeCheck(RuntimeCheckErrorKind::NoSuchPublicFunction(_, _)) )); - let err_result = env + let err_result = exec_state .execute_contract( + &invoke_ctx, &QualifiedContractIdentifier::local("factorial").unwrap(), "compute", &symbols_from_values(vec![Value::Bool(true)]), @@ -994,12 +1094,23 @@ fn test_at_unknown_block( ) .unwrap_err(); eprintln!("{err}"); - match err { - ClarityEvalError::Vm(VmExecutionError::Runtime(x, _)) => assert_eq!( - x, - RuntimeError::UnknownBlockHeaderHash(BlockHeaderHash::from(vec![2_u8; 32].as_slice())) - ), - e => panic!("Unexpected error: {e}"), + if epoch.supports_at_block() { + match err { + ClarityEvalError::Vm(VmExecutionError::Runtime(x, _)) => assert_eq!( + x, + RuntimeError::UnknownBlockHeaderHash(BlockHeaderHash::from( + vec![2_u8; 32].as_slice() + )) + ), + e => panic!("Unexpected error: {e}"), + } + } else { + match err { + ClarityEvalError::Vm(VmExecutionError::RuntimeCheck(x)) => { + assert_eq!(x, RuntimeCheckErrorKind::AtBlockUnavailable) + } + e => panic!("Unexpected error: {e}"), + } } } @@ -1091,15 +1202,18 @@ fn test_cc_stack_depth( let contract_two = "(unwrap-panic (contract-call? .c-foo foo))"; let placeholder_context = ContractContext::new(QualifiedContractIdentifier::transient(), version); - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); let contract_identifier = QualifiedContractIdentifier::local("c-foo").unwrap(); - env.initialize_contract(contract_identifier, &contract_one) + exec_state + .initialize_contract(&invoke_ctx, contract_identifier, &contract_one) .unwrap(); let contract_identifier = QualifiedContractIdentifier::local("c-bar").unwrap(); assert_eq!( - env.initialize_contract(contract_identifier, contract_two) + exec_state + .initialize_contract(&invoke_ctx, contract_identifier, contract_two) .unwrap_err(), RuntimeError::MaxStackDepthReached.into() ); @@ -1132,15 +1246,18 @@ fn test_cc_trait_stack_depth( let placeholder_context = ContractContext::new(QualifiedContractIdentifier::transient(), version); - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); let contract_identifier = QualifiedContractIdentifier::local("c-foo").unwrap(); - env.initialize_contract(contract_identifier, &contract_one) + exec_state + .initialize_contract(&invoke_ctx, contract_identifier, &contract_one) .unwrap(); let contract_identifier = QualifiedContractIdentifier::local("c-bar").unwrap(); assert_eq!( - env.initialize_contract(contract_identifier, contract_two) + exec_state + .initialize_contract(&invoke_ctx, contract_identifier, contract_two) .unwrap_err(), RuntimeError::MaxStackDepthReached.into() ); @@ -1158,13 +1275,14 @@ fn test_eval_with_non_existing_contract( ClarityVersion::Clarity2, ); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(get_principal().expect_principal().unwrap()), None, &placeholder_context, ); - let result = env.eval_read_only( + let result = exec_state.eval_read_only( + &invoke_ctx, &QualifiedContractIdentifier::local("absent").unwrap(), "(ok 0)", ); @@ -1177,7 +1295,6 @@ fn test_eval_with_non_existing_contract( )) .into() ); - drop(env); owned_env.commit().unwrap(); assert!(owned_env.destruct().is_some()); } @@ -1196,14 +1313,16 @@ fn test_contract_hash_success( let mut owned_env = env_factory.get_env(epoch); let placeholder_context = ContractContext::new(QualifiedContractIdentifier::transient(), version); - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); // Deploy a contract to hash let other_contract = QualifiedContractIdentifier::local("other-contract").unwrap(); let contract_content = "(define-constant test-var 1)"; let expected_hash = Sha512Trunc256Sum::from_data(contract_content.as_bytes()); - env.initialize_contract(other_contract.clone(), contract_content) + exec_state + .initialize_contract(&invoke_ctx, other_contract.clone(), contract_content) .unwrap(); // Test successful contract hash retrieval @@ -1211,14 +1330,16 @@ fn test_contract_hash_success( let test_program = "(define-read-only (get-hash (contract principal)) (contract-hash? contract))"; - env.initialize_contract(test_contract.clone(), test_program) + exec_state + .initialize_contract(&invoke_ctx, test_contract.clone(), test_program) .unwrap(); // Attempt to get the hash of the other contract and expect it to be // successful and for the returned hash to match the expected hash. let standard_principal = QualifiedContractIdentifier::local("standard-principal").unwrap(); - let result = env + let result = exec_state .execute_contract( + &invoke_ctx, &test_contract, "get-hash", &symbols_from_values(vec![Value::Principal(PrincipalData::Contract( @@ -1250,14 +1371,16 @@ fn test_contract_hash_nonexistent_contract( let mut owned_env = env_factory.get_env(epoch); let placeholder_context = ContractContext::new(QualifiedContractIdentifier::transient(), version); - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); // Deploy a contract to hash let other_contract = QualifiedContractIdentifier::local("other-contract").unwrap(); let contract_content = "(define-constant test-var 1)"; let expected_hash = Sha512Trunc256Sum::from_data(contract_content.as_bytes()); - env.initialize_contract(other_contract.clone(), contract_content) + exec_state + .initialize_contract(&invoke_ctx, other_contract.clone(), contract_content) .unwrap(); // Test successful contract hash retrieval @@ -1265,13 +1388,15 @@ fn test_contract_hash_nonexistent_contract( let test_program = "(define-read-only (get-hash (contract principal)) (contract-hash? contract))"; - env.initialize_contract(test_contract.clone(), test_program) + exec_state + .initialize_contract(&invoke_ctx, test_contract.clone(), test_program) .unwrap(); // Attempt to get the hash of a non-existent contract, expecting an `(err u2)` let non_existent_contract = QualifiedContractIdentifier::local("nonexistent-contract").unwrap(); - let result = env + let result = exec_state .execute_contract( + &invoke_ctx, &test_contract, "get-hash", &symbols_from_values(vec![Value::Principal(PrincipalData::Contract( @@ -1298,14 +1423,16 @@ fn test_contract_hash_standard_principal( let mut owned_env = env_factory.get_env(epoch); let placeholder_context = ContractContext::new(QualifiedContractIdentifier::transient(), version); - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); // Deploy a contract to hash let other_contract = QualifiedContractIdentifier::local("other-contract").unwrap(); let contract_content = "(define-constant test-var 1)"; let expected_hash = Sha512Trunc256Sum::from_data(contract_content.as_bytes()); - env.initialize_contract(other_contract.clone(), contract_content) + exec_state + .initialize_contract(&invoke_ctx, other_contract.clone(), contract_content) .unwrap(); // Test successful contract hash retrieval @@ -1313,12 +1440,14 @@ fn test_contract_hash_standard_principal( let test_program = "(define-read-only (get-hash (contract principal)) (contract-hash? contract))"; - env.initialize_contract(test_contract.clone(), test_program) + exec_state + .initialize_contract(&invoke_ctx, test_contract.clone(), test_program) .unwrap(); // Attempt to get the hash of a standard principal, expecting an `(err u1)` - let result = env + let result = exec_state .execute_contract( + &invoke_ctx, &test_contract, "get-hash", &symbols_from_values(vec![Value::Principal(PrincipalData::Standard( @@ -1345,24 +1474,26 @@ fn test_contract_hash_type_check( let mut owned_env = env_factory.get_env(epoch); let placeholder_context = ContractContext::new(QualifiedContractIdentifier::transient(), version); - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); // Deploy a contract with a type-check error in the `contract-hash?` expression // Note that this would usually fail in analysis, but we've skipped it here. let test_contract = QualifiedContractIdentifier::local("test-contract").unwrap(); let test_program = "(define-read-only (get-hash) (contract-hash? u123))"; - env.initialize_contract(test_contract.clone(), test_program) + exec_state + .initialize_contract(&invoke_ctx, test_contract.clone(), test_program) .unwrap(); // Attempt to execute the contract, expecting a type-check error - let err = env - .execute_contract(&test_contract, "get-hash", &[], true) + let err = exec_state + .execute_contract(&invoke_ctx, &test_contract, "get-hash", &[], true) .unwrap_err(); assert_eq!( err, VmExecutionError::RuntimeCheck(RuntimeCheckErrorKind::ExpectedContractPrincipalValue( - Box::new(Value::UInt(123)) + Value::UInt(123).to_error_string() )) ); } @@ -1381,14 +1512,16 @@ fn test_contract_hash_pre_clarity4( let mut owned_env = env_factory.get_env(epoch); let placeholder_context = ContractContext::new(QualifiedContractIdentifier::transient(), version); - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); // Deploy a contract to hash let other_contract = QualifiedContractIdentifier::local("other-contract").unwrap(); let contract_content = "(define-constant test-var 1)"; let expected_hash = Sha512Trunc256Sum::from_data(contract_content.as_bytes()); - env.initialize_contract(other_contract.clone(), contract_content) + exec_state + .initialize_contract(&invoke_ctx, other_contract.clone(), contract_content) .unwrap(); // Test successful contract hash retrieval @@ -1396,14 +1529,16 @@ fn test_contract_hash_pre_clarity4( let test_program = "(define-read-only (get-hash (contract principal)) (contract-hash? contract))"; - env.initialize_contract(test_contract.clone(), test_program) + exec_state + .initialize_contract(&invoke_ctx, test_contract.clone(), test_program) .unwrap(); // Attempt to get the hash of the other contract and expect it to be // successful and for the returned hash to match the expected hash. let standard_principal = QualifiedContractIdentifier::local("standard-principal").unwrap(); - let err = env + let err = exec_state .execute_contract( + &invoke_ctx, &test_contract, "get-hash", &symbols_from_values(vec![Value::Principal(PrincipalData::Contract( @@ -1441,25 +1576,31 @@ fn test_contract_call_with_constant( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("contract-a").unwrap(), - contract_a, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("contract-b").unwrap(), - contract_b, - ) - .unwrap(); + let (mut exec_env, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_env + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-a").unwrap(), + contract_a, + ) + .unwrap(); + exec_env + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-b").unwrap(), + contract_b, + ) + .unwrap(); } - let mut env = owned_env.get_exec_environment( + let (mut exec_env, invoke_ctx) = owned_env.get_exec_environment( Some(p1.clone().expect_principal().unwrap()), None, &placeholder_context, ); - let call_result = env.execute_contract( + let call_result = exec_env.execute_contract( + &invoke_ctx, &QualifiedContractIdentifier::local("contract-b").unwrap(), "call-foo", &[], @@ -1476,6 +1617,109 @@ fn test_contract_call_with_constant( } } +/// Calling from a deploying contract into a contract via define-constant +/// should not be allowed in any epochs +#[apply(test_clarity_versions)] +fn test_contract_call_with_constant_at_deploy( + version: ClarityVersion, + epoch: StacksEpochId, + mut env_factory: MemoryEnvironmentGenerator, +) { + let mut owned_env = env_factory.get_env(epoch); + + let contract_a = "(define-public (foo) (ok true))"; + let contract_b = "(define-constant MY_CONTRACT .contract-a) + (define-public (call-foo) + (contract-call? MY_CONTRACT foo) + ) + (call-foo) + "; + + let placeholder_context = + ContractContext::new(QualifiedContractIdentifier::transient(), version); + + let (mut exec_env, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_env + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-a").unwrap(), + contract_a, + ) + .unwrap(); + let call_result = exec_env.initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-b").unwrap(), + contract_b, + ); + + assert_eq!( + call_result.unwrap_err(), + ClarityEvalError::Vm(VmExecutionError::RuntimeCheck( + RuntimeCheckErrorKind::ContractCallExpectName + )), + ); +} + +/// Calling from a deploying contract into a contract which uses contract-calls via define-constant +/// should be allowed in appropriate epochs +#[apply(test_clarity_versions)] +fn test_nested_cc_with_constant_at_deploy( + version: ClarityVersion, + epoch: StacksEpochId, + mut env_factory: MemoryEnvironmentGenerator, +) { + let mut owned_env = env_factory.get_env(epoch); + + let contract_a = "(define-public (foo) (ok true))"; + let contract_b = "(define-constant MY_CONTRACT .contract-a) + (define-public (call-foo) + (contract-call? MY_CONTRACT foo) + ) + "; + let contract_c = " + (define-public (call-call-foo) + (contract-call? .contract-b call-foo)) + (call-call-foo) + "; + + let placeholder_context = + ContractContext::new(QualifiedContractIdentifier::transient(), version); + + let (mut exec_env, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_env + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-a").unwrap(), + contract_a, + ) + .unwrap(); + exec_env + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-b").unwrap(), + contract_b, + ) + .unwrap(); + let call_result = exec_env.initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-c").unwrap(), + contract_c, + ); + + if epoch.supports_call_with_constant() && version.supports_callables() { + call_result.unwrap(); + } else { + assert_eq!( + call_result.unwrap_err(), + ClarityEvalError::Vm(VmExecutionError::RuntimeCheck( + RuntimeCheckErrorKind::ContractCallExpectName + )), + ); + } +} + #[apply(test_clarity_versions)] fn test_constant_to_trait( version: ClarityVersion, @@ -1500,25 +1744,31 @@ fn test_constant_to_trait( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("contract-a").unwrap(), - contract_a, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("contract-b").unwrap(), - contract_b, - ) - .unwrap(); + let (mut exec_env, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_env + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-a").unwrap(), + contract_a, + ) + .unwrap(); + exec_env + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-b").unwrap(), + contract_b, + ) + .unwrap(); } - let mut env = owned_env.get_exec_environment( + let (mut exec_env, invoke_ctx) = owned_env.get_exec_environment( Some(p1.clone().expect_principal().unwrap()), None, &placeholder_context, ); - let call_result = env.execute_contract( + let call_result = exec_env.execute_contract( + &invoke_ctx, &QualifiedContractIdentifier::local("contract-b").unwrap(), "call-foo", &[], @@ -1527,3 +1777,199 @@ fn test_constant_to_trait( assert_eq!(call_result.unwrap(), Value::okay_true()); } + +/// Contract principal constants must work with principal-inspecting functions +/// (`is-standard`, `principal-destruct?`, `to-ascii?`). These functions +/// pattern-match on `Value::Principal` and previously failed when constants were +/// rewritten to `Value::CallableContract`. +/// +/// Skips Clarity1 because `is-standard` and `principal-destruct?` are not +/// available. Runs in all epochs for Clarity2+ because `define-constant` +/// with a contract principal literal always stores a `Value::Principal`. +#[apply(test_clarity_versions)] +fn test_constant_contract_principal_in_principal_functions( + version: ClarityVersion, + epoch: StacksEpochId, + mut env_factory: MemoryEnvironmentGenerator, +) { + if version < ClarityVersion::Clarity2 { + // Clarity1 does not have is-standard or principal-destruct? + return; + } + + let mut owned_env = env_factory.get_env(epoch); + + let contract_a = "(define-public (ping) (ok true))"; + let has_to_ascii = version >= ClarityVersion::Clarity4; + let contract_b = if has_to_ascii { + " + (define-constant TARGET .contract-a) + (define-read-only (check-standard) + (is-standard TARGET)) + (define-read-only (check-destruct) + (principal-destruct? TARGET)) + (define-read-only (check-to-ascii) + (to-ascii? TARGET)) + " + } else { + " + (define-constant TARGET .contract-a) + (define-read-only (check-standard) + (is-standard TARGET)) + (define-read-only (check-destruct) + (principal-destruct? TARGET)) + " + }; + + let placeholder_context = + ContractContext::new(QualifiedContractIdentifier::transient(), version); + + { + let (mut exec_env, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_env + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-a").unwrap(), + contract_a, + ) + .unwrap(); + exec_env + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-b").unwrap(), + contract_b, + ) + .unwrap(); + } + + let p1 = execute("'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR"); + let (mut exec_env, invoke_ctx) = owned_env.get_exec_environment( + Some(p1.expect_principal().unwrap()), + None, + &placeholder_context, + ); + + let contract_b_id = QualifiedContractIdentifier::local("contract-b").unwrap(); + + // is-standard returns false because the local test principal uses a + // non-standard version byte (0x01). + let result = exec_env + .execute_contract(&invoke_ctx, &contract_b_id, "check-standard", &[], false) + .unwrap(); + assert_eq!(result, Value::Bool(false)); + + // principal-destruct? returns (err ...) because version byte 0x01 is not + // a recognized network version. The tuple still contains the decomposed + // principal fields. + let result = exec_env + .execute_contract(&invoke_ctx, &contract_b_id, "check-destruct", &[], false) + .unwrap(); + assert_eq!( + result, + execute( + "(err { version: 0x01, hash-bytes: 0x0101010101010101010101010101010101010101, name: (some \"contract-a\") })" + ) + ); + + // to-ascii? returns (ok ) with the full principal representation. + if has_to_ascii { + let result = exec_env + .execute_contract(&invoke_ctx, &contract_b_id, "check-to-ascii", &[], false) + .unwrap(); + assert_eq!( + result, + execute("(ok \"S1G2081040G2081040G2081040G208105NK8PE5.contract-a\")") + ); + } +} + +/// A constant contract principal can be used as BOTH a contract-call? target +/// AND a principal argument to native functions within the same contract. +/// +/// In unsupported epochs, `contract-call?` via a constant fails with +/// `ContractCallExpectName`, but the principal-accepting functions +/// (`stx-get-balance`, `is-standard`) still work because the constant +/// evaluates to `Value::Principal`. +/// +/// Skips Clarity1 because `is-standard` is not available. +#[apply(test_clarity_versions)] +fn test_constant_contract_principal_dual_use( + version: ClarityVersion, + epoch: StacksEpochId, + mut env_factory: MemoryEnvironmentGenerator, +) { + if version < ClarityVersion::Clarity2 { + // Clarity1 does not have is-standard + return; + } + let mut owned_env = env_factory.get_env(epoch); + + let contract_a = " + (define-public (foo) (ok true)) + "; + let contract_b = " + (define-constant TARGET .contract-a) + (define-public (call-it) + (contract-call? TARGET foo)) + (define-read-only (get-bal) + (stx-get-balance TARGET)) + (define-read-only (check-standard) + (is-standard TARGET)) + "; + + let placeholder_context = + ContractContext::new(QualifiedContractIdentifier::transient(), version); + + { + let (mut exec_env, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_env + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-a").unwrap(), + contract_a, + ) + .unwrap(); + exec_env + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-b").unwrap(), + contract_b, + ) + .unwrap(); + } + + let p1 = execute("'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR"); + let (mut exec_env, invoke_ctx) = owned_env.get_exec_environment( + Some(p1.expect_principal().unwrap()), + None, + &placeholder_context, + ); + let contract_b_id = QualifiedContractIdentifier::local("contract-b").unwrap(); + + // contract-call? via constant requires epoch + version support + let call_result = exec_env.execute_contract(&invoke_ctx, &contract_b_id, "call-it", &[], false); + if epoch.supports_call_with_constant() && version.supports_callables() { + assert_eq!(call_result.unwrap(), Value::okay_true()); + } else { + assert_eq!( + call_result.unwrap_err(), + VmExecutionError::RuntimeCheck(RuntimeCheckErrorKind::ContractCallExpectName) + ); + } + + // stx-get-balance and is-standard work in all epochs because the + // constant is always `Value::Principal`. + let result = exec_env + .execute_contract(&invoke_ctx, &contract_b_id, "get-bal", &[], false) + .unwrap(); + assert_eq!(result, Value::UInt(0)); + + // is-standard returns false because the local test principal uses a + // non-standard version byte (0x01). + let result = exec_env + .execute_contract(&invoke_ctx, &contract_b_id, "check-standard", &[], false) + .unwrap(); + assert_eq!(result, Value::Bool(false)); +} diff --git a/clarity/src/vm/tests/conversions.rs b/clarity/src/vm/tests/conversions.rs index 0e4ec320e47..f57db2f1993 100644 --- a/clarity/src/vm/tests/conversions.rs +++ b/clarity/src/vm/tests/conversions.rs @@ -65,11 +65,10 @@ fn test_simple_buff_to_int_le() { Box::new(SequenceType(BufferType( BufferLength::try_from(16_u32).unwrap() ))), - Box::new(Value::Sequence(SequenceData::String(CharType::ASCII( - ASCIIData { - data: "wrong-type".as_bytes().to_vec() - } - )))) + Value::Sequence(SequenceData::String(CharType::ASCII(ASCIIData { + data: "wrong-type".as_bytes().to_vec() + }))) + .to_error_string() ) .into() ); @@ -82,9 +81,10 @@ fn test_simple_buff_to_int_le() { Box::new(SequenceType(BufferType( BufferLength::try_from(16_u32).unwrap() ))), - Box::new(Value::Sequence(SequenceData::Buffer(BuffData { + Value::Sequence(SequenceData::Buffer(BuffData { data: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0] - }))) + })) + .to_error_string() ) .into() ); @@ -123,11 +123,10 @@ fn test_simple_buff_to_uint_le() { Box::new(SequenceType(BufferType( BufferLength::try_from(16_u32).unwrap() ))), - Box::new(Value::Sequence(SequenceData::String(CharType::ASCII( - ASCIIData { - data: "wrong-type".as_bytes().to_vec() - } - )))) + Value::Sequence(SequenceData::String(CharType::ASCII(ASCIIData { + data: "wrong-type".as_bytes().to_vec() + }))) + .to_error_string() ) .into() ); @@ -140,9 +139,10 @@ fn test_simple_buff_to_uint_le() { Box::new(SequenceType(BufferType( BufferLength::try_from(16_u32).unwrap() ))), - Box::new(Value::Sequence(SequenceData::Buffer(BuffData { + Value::Sequence(SequenceData::Buffer(BuffData { data: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0] - }))) + })) + .to_error_string() ) .into() ); @@ -181,11 +181,10 @@ fn test_simple_buff_to_int_be() { Box::new(SequenceType(BufferType( BufferLength::try_from(16_u32).unwrap() ))), - Box::new(Value::Sequence(SequenceData::String(CharType::ASCII( - ASCIIData { - data: "wrong-type".as_bytes().to_vec() - } - )))) + Value::Sequence(SequenceData::String(CharType::ASCII(ASCIIData { + data: "wrong-type".as_bytes().to_vec() + }))) + .to_error_string() ) .into() ); @@ -198,9 +197,10 @@ fn test_simple_buff_to_int_be() { Box::new(SequenceType(BufferType( BufferLength::try_from(16_u32).unwrap() ))), - Box::new(Value::Sequence(SequenceData::Buffer(BuffData { + Value::Sequence(SequenceData::Buffer(BuffData { data: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0] - }))) + })) + .to_error_string() ) .into() ); @@ -239,11 +239,10 @@ fn test_simple_buff_to_uint_be() { Box::new(SequenceType(BufferType( BufferLength::try_from(16_u32).unwrap() ))), - Box::new(Value::Sequence(SequenceData::String(CharType::ASCII( - ASCIIData { - data: "wrong-type".as_bytes().to_vec() - } - )))) + Value::Sequence(SequenceData::String(CharType::ASCII(ASCIIData { + data: "wrong-type".as_bytes().to_vec() + }))) + .to_error_string() ) .into() ); @@ -256,9 +255,10 @@ fn test_simple_buff_to_uint_be() { Box::new(SequenceType(BufferType( BufferLength::try_from(16_u32).unwrap() ))), - Box::new(Value::Sequence(SequenceData::Buffer(BuffData { + Value::Sequence(SequenceData::Buffer(BuffData { data: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0] - }))) + })) + .to_error_string() ) .into() ); @@ -323,7 +323,7 @@ fn test_simple_string_to_int() { TypeSignature::STRING_ASCII_MAX, TypeSignature::STRING_UTF8_MAX, ], - Box::new(Value::Int(1)) + Value::Int(1).to_error_string() ) .into() ); @@ -388,7 +388,7 @@ fn test_simple_string_to_uint() { TypeSignature::STRING_ASCII_MAX, TypeSignature::STRING_UTF8_MAX, ], - Box::new(Value::Int(1)) + Value::Int(1).to_error_string() ) .into() ); @@ -419,11 +419,10 @@ fn test_simple_int_to_ascii() { execute_v2(wrong_type_error_test).unwrap_err(), RuntimeCheckErrorKind::UnionTypeValueError( vec![TypeSignature::IntType, TypeSignature::UIntType], - Box::new(Value::Sequence(SequenceData::String(CharType::ASCII( - ASCIIData { - data: "1".as_bytes().to_vec() - } - )))) + Value::Sequence(SequenceData::String(CharType::ASCII(ASCIIData { + data: "1".as_bytes().to_vec() + }))) + .to_error_string() ) .into() ); @@ -454,11 +453,10 @@ fn test_simple_int_to_utf8() { execute_v2(wrong_type_error_test).unwrap_err(), RuntimeCheckErrorKind::UnionTypeValueError( vec![TypeSignature::IntType, TypeSignature::UIntType], - Box::new(Value::Sequence(SequenceData::String(CharType::ASCII( - ASCIIData { - data: "1".as_bytes().to_vec() - } - )))) + Value::Sequence(SequenceData::String(CharType::ASCII(ASCIIData { + data: "1".as_bytes().to_vec() + }))) + .to_error_string() ) .into() ); diff --git a/clarity/src/vm/tests/crypto.rs b/clarity/src/vm/tests/crypto.rs index 40c97a6694b..01bb8f4eb35 100644 --- a/clarity/src/vm/tests/crypto.rs +++ b/clarity/src/vm/tests/crypto.rs @@ -193,8 +193,8 @@ fn secp256r1_verify_valid_signatures_nist() { Value::Bool(true), execute_with_parameters( program.as_str(), - ClarityVersion::Clarity5, - StacksEpochId::Epoch34, + ClarityVersion::latest(), + StacksEpochId::latest(), false ) .expect("execution should succeed") @@ -203,6 +203,8 @@ fn secp256r1_verify_valid_signatures_nist() { } } +/// Returns (message_hash, signature, pubkey) for secp256r1 using double-hash signing +/// (Clarity 4 and earlier behavior: `sign()` hashes the message again internally). fn secp256r1_vectors() -> (Vec, Vec, Vec) { let privk = Secp256r1PrivateKey::from_seed(&[7u8; 32]); let pubk = Secp256r1PublicKey::from_private(&privk); @@ -218,6 +220,23 @@ fn secp256r1_vectors() -> (Vec, Vec, Vec) { ) } +/// Returns (message_hash, signature, pubkey) for secp256r1 using digest signing +/// (Clarity 5+ behavior: `sign_digest()` uses the message hash directly without re-hashing). +fn secp256r1_vectors_digest() -> (Vec, Vec, Vec) { + let privk = Secp256r1PrivateKey::from_seed(&[7u8; 32]); + let pubk = Secp256r1PublicKey::from_private(&privk); + let message_hash = Sha256Sum::from_data(b"clarity-secp256r1-tests"); + let signature = privk + .sign_digest(message_hash.as_bytes()) + .expect("secp256r1 digest signing should succeed"); + + ( + message_hash.as_bytes().to_vec(), + signature.0.to_vec(), + pubk.to_bytes_compressed(), + ) +} + fn secp256k1_vectors() -> (Vec, Vec, Vec) { let privk = StacksPrivateKey::from_seed(&[9u8; 32]); let pubk = StacksPublicKey::from_private(&privk); @@ -245,6 +264,7 @@ fn zeroed_buff_literal(len: usize) -> String { #[test] fn test_secp256r1_verify_valid_signature_returns_true() { + // Clarity 4 (double-hash): sign() hashes internally, secp256r1-verify hashes again let (message, signature, pubkey) = secp256r1_vectors(); let program = format!( "(secp256r1-verify {} {} {})", @@ -253,6 +273,40 @@ fn test_secp256r1_verify_valid_signature_returns_true() { buff_literal(&pubkey) ); + assert_eq!( + Value::Bool(true), + execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false + ) + .expect("execution should succeed") + .expect("should return a value") + ); + + // Same double-hash signature must NOT verify under Clarity 5+ (direct digest) + assert_eq!( + Value::Bool(false), + execute_with_parameters( + program.as_str(), + ClarityVersion::latest(), + StacksEpochId::latest(), + false + ) + .expect("execution should succeed") + .expect("should return a value") + ); + + // Clarity 5+ (direct digest): sign_digest() signs the hash directly + let (message, signature, pubkey) = secp256r1_vectors_digest(); + let program = format!( + "(secp256r1-verify {} {} {})", + buff_literal(&message), + buff_literal(&signature), + buff_literal(&pubkey) + ); + assert_eq!( Value::Bool(true), execute_with_parameters( @@ -264,16 +318,62 @@ fn test_secp256r1_verify_valid_signature_returns_true() { .expect("execution should succeed") .expect("should return a value") ); + + // Same digest signature must NOT verify under Clarity 4 (double-hash) + assert_eq!( + Value::Bool(false), + execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false + ) + .expect("execution should succeed") + .expect("should return a value") + ); } #[test] fn test_secp256r1_verify_valid_high_s_signature_returns_true() { - let message = "0xc3abef6a775793dfbc8e0719e7a1de1fc2f90d37a7912b1ce8e300a5a03b06a8"; - let signature = "0xf2b8c0645caa7250e3b96d633cf40a88456e4ffbddffb69200c4e019039dfd31f153a6d5c3dc192a5574f3a261b1b70570971b92d8ebf86c17b7670d13591c4e"; - let pubkey = "0x031e18532fd4754c02f3041d9c75ceb33b83ffd81ac7ce4fe882ccb1c98bc5896e"; + use stacks_common::util::secp256r1::MessageSignature; - let program = format!("(secp256r1-verify {message} {signature} {pubkey})"); + // secp256r1-verify accepts high-S signatures (unlike secp256k1-verify). + // Clarity 4 (double-hash path) + let (message, signature, pubkey) = secp256r1_vectors(); + let high_s_sig = MessageSignature(signature.as_slice().try_into().unwrap()) + .to_high_s() + .expect("should create high-S signature"); + let program = format!( + "(secp256r1-verify {} {} {})", + buff_literal(&message), + buff_literal(&high_s_sig.0), + buff_literal(&pubkey) + ); + assert_eq!( + Value::Bool(true), + execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false + ) + .expect("execution should succeed") + .expect("should return a value"), + "High-S signature should verify in Clarity 4" + ); + + // Clarity 5+ (direct digest path) + let (message, signature, pubkey) = secp256r1_vectors_digest(); + let high_s_sig = MessageSignature(signature.as_slice().try_into().unwrap()) + .to_high_s() + .expect("should create high-S signature"); + let program = format!( + "(secp256r1-verify {} {} {})", + buff_literal(&message), + buff_literal(&high_s_sig.0), + buff_literal(&pubkey) + ); assert_eq!( Value::Bool(true), execute_with_parameters( @@ -283,22 +383,43 @@ fn test_secp256r1_verify_valid_high_s_signature_returns_true() { false ) .expect("execution should succeed") - .expect("should return a value") + .expect("should return a value"), + "High-S signature should verify in Clarity 5+" ); } #[test] fn test_secp256r1_verify_invalid_signature_returns_false() { + // Clarity 4 (double-hash) let (message, mut signature, pubkey) = secp256r1_vectors(); signature[0] ^= 0x01; - let program = format!( "(secp256r1-verify {} {} {})", buff_literal(&message), buff_literal(&signature), buff_literal(&pubkey) ); + assert_eq!( + Value::Bool(false), + execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false + ) + .expect("execution should succeed") + .expect("should return a value") + ); + // Clarity 5+ (direct digest) + let (message, mut signature, pubkey) = secp256r1_vectors_digest(); + signature[0] ^= 0x01; + let program = format!( + "(secp256r1-verify {} {} {})", + buff_literal(&message), + buff_literal(&signature), + buff_literal(&pubkey) + ); assert_eq!( Value::Bool(false), execute_with_parameters( @@ -314,16 +435,36 @@ fn test_secp256r1_verify_invalid_signature_returns_false() { #[test] fn test_secp256r1_verify_signature_too_short_returns_false() { + // Clarity 4 (double-hash) let (message, mut signature, pubkey) = secp256r1_vectors(); signature.truncate(63); - let program = format!( "(secp256r1-verify {} {} {})", buff_literal(&message), buff_literal(&signature), buff_literal(&pubkey) ); + assert_eq!( + Value::Bool(false), + execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false + ) + .expect("execution should succeed") + .expect("should return a value") + ); + // Clarity 5+ (direct digest) + let (message, mut signature, pubkey) = secp256r1_vectors_digest(); + signature.truncate(63); + let program = format!( + "(secp256r1-verify {} {} {})", + buff_literal(&message), + buff_literal(&signature), + buff_literal(&pubkey) + ); assert_eq!( Value::Bool(false), execute_with_parameters( @@ -339,16 +480,40 @@ fn test_secp256r1_verify_signature_too_short_returns_false() { #[test] fn test_secp256r1_verify_signature_too_long_errors() { + // Clarity 4 (double-hash) let (message, mut signature, pubkey) = secp256r1_vectors(); signature.push(0x00); - let program = format!( "(secp256r1-verify {} {} {})", buff_literal(&message), buff_literal(&signature), buff_literal(&pubkey) ); + let err = execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false, + ) + .unwrap_err(); + match err { + ClarityEvalError::Vm(VmExecutionError::RuntimeCheck( + RuntimeCheckErrorKind::TypeValueError(expected, _), + )) => { + assert_eq!(*expected, TypeSignature::BUFFER_64); + } + _ => panic!("expected BUFFER_64 type error, found {err:?}"), + } + // Clarity 5+ (direct digest) + let (message, mut signature, pubkey) = secp256r1_vectors_digest(); + signature.push(0x00); + let program = format!( + "(secp256r1-verify {} {} {})", + buff_literal(&message), + buff_literal(&signature), + buff_literal(&pubkey) + ); let err = execute_with_parameters( program.as_str(), ClarityVersion::latest(), @@ -362,7 +527,7 @@ fn test_secp256r1_verify_signature_too_long_errors() { )) => { assert_eq!(*expected, TypeSignature::BUFFER_64); } - _ => panic!("expected BUFFER_65 type error, found {err:?}"), + _ => panic!("expected BUFFER_64 type error, found {err:?}"), } } @@ -640,6 +805,8 @@ proptest! { let pubk = Secp256r1PublicKey::from_private(&privk); let pubkey_bytes = pubk.to_bytes_compressed(); let message = message.to_vec(); + + // Clarity 4: sign() does double-hash let signature = privk.sign(&message).expect("secp256r1 signing should succeed"); let program = format!( "(secp256r1-verify {} {} {})", @@ -647,7 +814,24 @@ proptest! { buff_literal(&signature.0), buff_literal(&pubkey_bytes) ); + let result = execute_with_parameters( + program.as_str(), + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + false, + ) + .expect("execution should succeed") + .expect("should return a value"); + prop_assert_eq!(Value::Bool(true), result.clone(), "Clarity 4 double-hash verify failed"); + // Clarity 5+: sign_digest() uses hash directly + let signature = privk.sign_digest(&message).expect("secp256r1 digest signing should succeed"); + let program = format!( + "(secp256r1-verify {} {} {})", + buff_literal(&message), + buff_literal(&signature.0), + buff_literal(&pubkey_bytes) + ); let result = execute_with_parameters( program.as_str(), ClarityVersion::latest(), @@ -656,8 +840,7 @@ proptest! { ) .expect("execution should succeed") .expect("should return a value"); - - prop_assert_eq!(Value::Bool(true), result); + prop_assert_eq!(Value::Bool(true), result, "Clarity 5+ digest verify failed"); } #[test] @@ -698,29 +881,35 @@ proptest! { let privk = Secp256r1PrivateKey::from_seed(&seed); let pubk = Secp256r1PublicKey::from_private(&privk); let pubkey_bytes = pubk.to_bytes_compressed(); - let mut message = message.to_vec(); - let signature = privk.sign(&message).expect("secp256r1 signing should succeed"); - - // flip one bit - message[bit] ^= 0x01; + let message = message.to_vec(); + // Clarity 4: sign() does double-hash + let signature = privk.sign(&message).expect("secp256r1 signing should succeed"); + let mut tampered = message.clone(); + tampered[bit] ^= 0x01; let program = format!( "(secp256r1-verify {} {} {})", - buff_literal(&message), + buff_literal(&tampered), buff_literal(&signature.0), buff_literal(&pubkey_bytes) ); - let result = execute_with_parameters( - program.as_str(), - ClarityVersion::latest(), - StacksEpochId::latest(), - false, - ) - .expect("execution should succeed") - .expect("should return a value"); + &program, ClarityVersion::Clarity4, StacksEpochId::Epoch33, false + ).unwrap().unwrap(); + prop_assert_eq!(Value::Bool(false), result.clone(), "Clarity 4 tampered msg should fail"); - prop_assert_eq!(Value::Bool(false), result); + // Clarity 5+: sign_digest() uses hash directly + let signature = privk.sign_digest(&message).expect("secp256r1 digest signing should succeed"); + let program = format!( + "(secp256r1-verify {} {} {})", + buff_literal(&tampered), + buff_literal(&signature.0), + buff_literal(&pubkey_bytes) + ); + let result = execute_with_parameters( + &program, ClarityVersion::latest(), StacksEpochId::latest(), false + ).unwrap().unwrap(); + prop_assert_eq!(Value::Bool(false), result, "Clarity 5+ tampered msg should fail"); } #[test] @@ -769,10 +958,23 @@ proptest! { let priv_a = Secp256r1PrivateKey::from_seed(&seed_a); let pub_b = Secp256r1PublicKey::from_private(&Secp256r1PrivateKey::from_seed(&seed_b)); let pub_b_bytes = pub_b.to_bytes_compressed(); - let msg = message.to_vec(); + + // Clarity 4: sign() does double-hash let signature = priv_a.sign(&msg).unwrap(); + let program = format!( + "(secp256r1-verify {} {} {})", + buff_literal(&msg), + buff_literal(&signature.0), + buff_literal(&pub_b_bytes) + ); + let result = execute_with_parameters( + &program, ClarityVersion::Clarity4, StacksEpochId::Epoch33, false + ).unwrap().unwrap(); + prop_assert_eq!(Value::Bool(false), result.clone(), "Clarity 4 wrong key should fail"); + // Clarity 5+: sign_digest() uses hash directly + let signature = priv_a.sign_digest(&msg).unwrap(); let program = format!( "(secp256r1-verify {} {} {})", buff_literal(&msg), @@ -782,8 +984,7 @@ proptest! { let result = execute_with_parameters( &program, ClarityVersion::latest(), StacksEpochId::latest(), false ).unwrap().unwrap(); - - prop_assert_eq!(Value::Bool(false), result); + prop_assert_eq!(Value::Bool(false), result, "Clarity 5+ wrong key should fail"); } #[test] diff --git a/clarity/src/vm/tests/defines.rs b/clarity/src/vm/tests/defines.rs index 699b815b430..86cf9e48b8b 100644 --- a/clarity/src/vm/tests/defines.rs +++ b/clarity/src/vm/tests/defines.rs @@ -72,7 +72,7 @@ fn test_accept_options(#[case] version: ClarityVersion, #[case] epoch: StacksEpo Ok(Some(Value::Int(10))), Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::from_string("(optional int)", version, epoch)), - Box::new(Value::some(Value::Bool(true)).unwrap()), + Value::some(Value::Bool(true)).unwrap().to_error_string(), ) .into()), ]; diff --git a/clarity/src/vm/tests/principals.rs b/clarity/src/vm/tests/principals.rs index 8e2c784cf4a..0d1c9b26646 100644 --- a/clarity/src/vm/tests/principals.rs +++ b/clarity/src/vm/tests/principals.rs @@ -39,8 +39,11 @@ fn test_simple_is_standard_check_inputs() { true ) .unwrap_err(), - RuntimeCheckErrorKind::TypeValueError(Box::new(PrincipalType), Box::new(Value::UInt(10)),) - .into() + RuntimeCheckErrorKind::TypeValueError( + Box::new(PrincipalType), + Value::UInt(10).to_error_string() + ) + .into() ); } @@ -922,9 +925,10 @@ fn test_principal_construct_runtime_check_errors() { assert_eq!( Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_1), - Box::new(Value::Sequence(SequenceData::Buffer(BuffData { + Value::Sequence(SequenceData::Buffer(BuffData { data: hex_bytes("590493").unwrap() - }))), + })) + .to_error_string(), ) .into()), execute_with_parameters( @@ -941,7 +945,7 @@ fn test_principal_construct_runtime_check_errors() { assert_eq!( Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_1), - Box::new(Value::UInt(22)), + Value::UInt(22).to_error_string(), ) .into()), execute_with_parameters( @@ -965,9 +969,10 @@ fn test_principal_construct_runtime_check_errors() { .unwrap_err(), RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_20), - Box::new(Value::Sequence(SequenceData::Buffer(BuffData { + Value::Sequence(SequenceData::Buffer(BuffData { data: hex_bytes("010203040506070809101112131415161718192021").unwrap() - }))), + })) + .to_error_string(), ) .into() ); @@ -977,13 +982,12 @@ fn test_principal_construct_runtime_check_errors() { assert_eq!( Err(RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::CONTRACT_NAME_STRING_ASCII_MAX), - Box::new(Value::Sequence(SequenceData::String(CharType::ASCII( - ASCIIData { - data: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - .as_bytes() - .to_vec() - } - )))) + Value::Sequence(SequenceData::String(CharType::ASCII(ASCIIData { + data: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + .as_bytes() + .to_vec() + }))) + .to_error_string() ) .into()), execute_with_parameters( diff --git a/clarity/src/vm/tests/sequences.rs b/clarity/src/vm/tests/sequences.rs index a05d0f5024d..40e498a4605 100644 --- a/clarity/src/vm/tests/sequences.rs +++ b/clarity/src/vm/tests/sequences.rs @@ -110,15 +110,15 @@ fn test_index_of() { )), RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::BUFFER_MIN), - Box::new(execute("\"a\"").unwrap().unwrap()), + execute("\"a\"").unwrap().unwrap().to_error_string(), ), RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::STRING_UTF8_MIN), - Box::new(execute("\"a\"").unwrap().unwrap()), + execute("\"a\"").unwrap().unwrap().to_error_string(), ), RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::STRING_ASCII_MIN), - Box::new(execute("u\"a\"").unwrap().unwrap()), + execute("u\"a\"").unwrap().unwrap().to_error_string(), ), ]; @@ -174,7 +174,7 @@ fn test_element_at() { )), RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::UIntType), - Box::new(Value::Int(1)), + Value::Int(1).to_error_string(), ), ]; @@ -755,14 +755,18 @@ fn test_simple_list_replace_at() { // The type of the index should be uint. assert_eq!( execute_v2("(replace-at? (list 1) 0 0)").unwrap_err(), - RuntimeCheckErrorKind::TypeValueError(Box::new(UIntType), Box::new(Value::Int(0))).into() + RuntimeCheckErrorKind::TypeValueError(Box::new(UIntType), Value::Int(0).to_error_string()) + .into() ); // The element input has the wrong type assert_eq!( execute_v2("(replace-at? (list 2 3) u0 true)").unwrap_err(), - RuntimeCheckErrorKind::TypeValueError(Box::new(IntType), Box::new(Value::Bool(true))) - .into() + RuntimeCheckErrorKind::TypeValueError( + Box::new(IntType), + Value::Bool(true).to_error_string() + ) + .into() ); // The element input has the wrong type @@ -770,7 +774,7 @@ fn test_simple_list_replace_at() { execute_v2("(replace-at? (list 2 3) u0 0x00)").unwrap_err(), RuntimeCheckErrorKind::TypeValueError( Box::new(IntType), - Box::new(Value::buff_from_byte(0)) + Value::buff_from_byte(0).to_error_string() ) .into() ); @@ -818,7 +822,8 @@ fn test_simple_buff_replace_at() { // The type of the index should be uint. assert_eq!( execute_v2("(replace-at? 0x002244 0 0x99)").unwrap_err(), - RuntimeCheckErrorKind::TypeValueError(Box::new(UIntType), Box::new(Value::Int(0))).into() + RuntimeCheckErrorKind::TypeValueError(Box::new(UIntType), Value::Int(0).to_error_string()) + .into() ); // The element input has the wrong type @@ -827,7 +832,7 @@ fn test_simple_buff_replace_at() { execute_v2("(replace-at? 0x445522 u0 55)").unwrap_err(), RuntimeCheckErrorKind::TypeValueError( Box::new(SequenceType(BufferType(buff_len.clone()))), - Box::new(Value::Int(55)) + Value::Int(55).to_error_string() ) .into() ); @@ -837,7 +842,9 @@ fn test_simple_buff_replace_at() { execute_v2("(replace-at? 0x445522 u0 (list 5))").unwrap_err(), RuntimeCheckErrorKind::TypeValueError( Box::new(SequenceType(BufferType(buff_len.clone()))), - Box::new(Value::list_from(vec![Value::Int(5)]).unwrap()) + Value::list_from(vec![Value::Int(5)]) + .unwrap() + .to_error_string() ) .into() ); @@ -847,7 +854,7 @@ fn test_simple_buff_replace_at() { execute_v2("(replace-at? 0x445522 u0 0x0044)").unwrap_err(), RuntimeCheckErrorKind::TypeValueError( Box::new(SequenceType(BufferType(buff_len))), - Box::new(Value::buff_from(vec![0, 68]).unwrap()) + Value::buff_from(vec![0, 68]).unwrap().to_error_string() ) .into() ); @@ -895,7 +902,8 @@ fn test_simple_string_ascii_replace_at() { // The type of the index should be uint. assert_eq!( execute_v2("(replace-at? \"abc\" 0 \"c\")").unwrap_err(), - RuntimeCheckErrorKind::TypeValueError(Box::new(UIntType), Box::new(Value::Int(0))).into() + RuntimeCheckErrorKind::TypeValueError(Box::new(UIntType), Value::Int(0).to_error_string()) + .into() ); // The element input has the wrong type @@ -904,7 +912,7 @@ fn test_simple_string_ascii_replace_at() { execute_v2("(replace-at? \"abc\" u0 55)").unwrap_err(), RuntimeCheckErrorKind::TypeValueError( Box::new(SequenceType(StringType(ASCII(buff_len.clone())))), - Box::new(Value::Int(55)) + Value::Int(55).to_error_string() ) .into() ); @@ -914,7 +922,7 @@ fn test_simple_string_ascii_replace_at() { execute_v2("(replace-at? \"abc\" u0 0x00)").unwrap_err(), RuntimeCheckErrorKind::TypeValueError( Box::new(SequenceType(StringType(ASCII(buff_len.clone())))), - Box::new(Value::buff_from_byte(0)) + Value::buff_from_byte(0).to_error_string() ) .into() ); @@ -924,7 +932,9 @@ fn test_simple_string_ascii_replace_at() { execute_v2("(replace-at? \"abc\" u0 \"de\")").unwrap_err(), RuntimeCheckErrorKind::TypeValueError( Box::new(SequenceType(StringType(ASCII(buff_len)))), - Box::new(Value::string_ascii_from_bytes("de".into()).unwrap()) + Value::string_ascii_from_bytes("de".into()) + .unwrap() + .to_error_string() ) .into() ); @@ -976,7 +986,8 @@ fn test_simple_string_utf8_replace_at() { // The type of the index should be uint. assert_eq!( execute_v2("(replace-at? u\"abc\" 0 u\"c\")").unwrap_err(), - RuntimeCheckErrorKind::TypeValueError(Box::new(UIntType), Box::new(Value::Int(0))).into() + RuntimeCheckErrorKind::TypeValueError(Box::new(UIntType), Value::Int(0).to_error_string()) + .into() ); // The element input has the wrong type @@ -987,7 +998,7 @@ fn test_simple_string_utf8_replace_at() { Box::new(TypeSignature::SequenceType(StringType( StringSubtype::UTF8(str_len.clone()) ))), - Box::new(Value::Int(55)) + Value::Int(55).to_error_string() ) .into() ); @@ -999,7 +1010,7 @@ fn test_simple_string_utf8_replace_at() { Box::new(TypeSignature::SequenceType(StringType( StringSubtype::UTF8(str_len.clone()) ))), - Box::new(Value::buff_from_byte(0)) + Value::buff_from_byte(0).to_error_string() ) .into() ); @@ -1011,7 +1022,9 @@ fn test_simple_string_utf8_replace_at() { Box::new(TypeSignature::SequenceType(StringType( StringSubtype::UTF8(str_len) ))), - Box::new(Value::string_utf8_from_string_utf8_literal("de".to_string()).unwrap()) + Value::string_utf8_from_string_utf8_literal("de".to_string()) + .unwrap() + .to_error_string() ) .into() ); diff --git a/clarity/src/vm/tests/simple_apply_eval.rs b/clarity/src/vm/tests/simple_apply_eval.rs index e6b57c47bd2..72503722126 100644 --- a/clarity/src/vm/tests/simple_apply_eval.rs +++ b/clarity/src/vm/tests/simple_apply_eval.rs @@ -28,7 +28,7 @@ use stacks_common::util::hash::{hex_bytes, to_hex}; use crate::vm::ast::parse; use crate::vm::callables::DefinedFunction; -use crate::vm::contexts::OwnedEnvironment; +use crate::vm::contexts::{ExecutionState, InvocationContext, OwnedEnvironment}; use crate::vm::costs::LimitedCostTracker; use crate::vm::database::MemoryBackingStore; use crate::vm::errors::{ @@ -41,8 +41,8 @@ use crate::vm::types::{ TypeSignature, }; use crate::vm::{ - CallStack, ClarityVersion, ContractContext, CostErrors, Environment, GlobalContext, - LocalContext, Value, eval, execute as vm_execute, execute_v2 as vm_execute_v2, + CallStack, ClarityVersion, ContractContext, CostErrors, GlobalContext, LocalContext, Value, + ValueRef, eval, execute as vm_execute, execute_v2 as vm_execute_v2, execute_with_limited_execution_time as vm_execute_with_limited_execution_time, execute_with_parameters, }; @@ -86,14 +86,11 @@ fn test_simple_let(#[case] version: ClarityVersion, #[case] epoch: StacksEpochId let context = LocalContext::new(); let mut marf = MemoryBackingStore::new(); let mut env = OwnedEnvironment::new(marf.as_clarity_db(), epoch); - + let (mut exec_state, invoke_ctx) = + env.get_exec_environment(None, None, &placeholder_context); assert_eq!( - Ok(Value::Int(7)), - eval( - &parsed_program[0], - &mut env.get_exec_environment(None, None, &placeholder_context), - &context - ) + Ok(ValueRef::Owned(Value::Int(7))), + eval(&parsed_program[0], &mut exec_state, &invoke_ctx, &context) ); } else { panic!("Failed to parse program."); @@ -307,7 +304,7 @@ fn test_from_consensus_buff_type_checks() { ), ( "(from-consensus-buff? uint 1)", - "RuntimeCheck(TypeValueError(SequenceType(BufferType(BufferLength(1048576))), Int(1)))", + "RuntimeCheck(TypeValueError(SequenceType(BufferType(BufferLength(1048576))), \"Int(1)\"))", ), ( "(from-consensus-buff? 2 0x10)", @@ -621,14 +618,14 @@ fn test_secp256k1_errors() { ]; let expectations: &[ClarityEvalError] = &[ - RuntimeCheckErrorKind::TypeValueError(Box::new(TypeSignature::BUFFER_32), Box::new(Value::Sequence(SequenceData::Buffer(BuffData { data: hex_bytes("de5b9eb9e7c5592930eb2e30a01369c36586d872082ed8181ee83d2a0ec20f").unwrap() })))).into(), - RuntimeCheckErrorKind::TypeValueError(Box::new(TypeSignature::BUFFER_65), Box::new(Value::Sequence(SequenceData::Buffer(BuffData { data: hex_bytes("8738487ebe69b93d8e51583be8eee50bb4213fc49c767d329632730cc193b873554428fc936ca3569afc15f1c9365f6591d6251a89fee9c9ac661116824d3a130100").unwrap() })))).into(), + RuntimeCheckErrorKind::TypeValueError(Box::new(TypeSignature::BUFFER_32), Value::Sequence(SequenceData::Buffer(BuffData { data: hex_bytes("de5b9eb9e7c5592930eb2e30a01369c36586d872082ed8181ee83d2a0ec20f").unwrap() })).to_error_string()).into(), + RuntimeCheckErrorKind::TypeValueError(Box::new(TypeSignature::BUFFER_65), Value::Sequence(SequenceData::Buffer(BuffData { data: hex_bytes("8738487ebe69b93d8e51583be8eee50bb4213fc49c767d329632730cc193b873554428fc936ca3569afc15f1c9365f6591d6251a89fee9c9ac661116824d3a130100").unwrap() })).to_error_string()).into(), RuntimeCheckErrorKind::IncorrectArgumentCount(2, 1).into(), RuntimeCheckErrorKind::IncorrectArgumentCount(2, 3).into(), - RuntimeCheckErrorKind::TypeValueError(Box::new(TypeSignature::BUFFER_32), Box::new(Value::Sequence(SequenceData::Buffer(BuffData { data: hex_bytes("de5b9eb9e7c5592930eb2e30a01369c36586d872082ed8181ee83d2a0ec20f").unwrap() })))).into(), - RuntimeCheckErrorKind::TypeValueError(Box::new(TypeSignature::BUFFER_65), Box::new(Value::Sequence(SequenceData::Buffer(BuffData { data: hex_bytes("8738487ebe69b93d8e51583be8eee50bb4213fc49c767d329632730cc193b873554428fc936ca3569afc15f1c9365f6591d6251a89fee9c9ac661116824d3a130111").unwrap() })))).into(), - RuntimeCheckErrorKind::TypeValueError(Box::new(TypeSignature::BUFFER_33), Box::new(Value::Sequence(SequenceData::Buffer(BuffData { data: hex_bytes("03adb8de4bfb65db2cfd6120d55c6526ae9c52e675db7e47308636534ba7").unwrap() })))).into(), + RuntimeCheckErrorKind::TypeValueError(Box::new(TypeSignature::BUFFER_32), Value::Sequence(SequenceData::Buffer(BuffData { data: hex_bytes("de5b9eb9e7c5592930eb2e30a01369c36586d872082ed8181ee83d2a0ec20f").unwrap() })).to_error_string()).into(), + RuntimeCheckErrorKind::TypeValueError(Box::new(TypeSignature::BUFFER_65), Value::Sequence(SequenceData::Buffer(BuffData { data: hex_bytes("8738487ebe69b93d8e51583be8eee50bb4213fc49c767d329632730cc193b873554428fc936ca3569afc15f1c9365f6591d6251a89fee9c9ac661116824d3a130111").unwrap() })).to_error_string()).into(), + RuntimeCheckErrorKind::TypeValueError(Box::new(TypeSignature::BUFFER_33), Value::Sequence(SequenceData::Buffer(BuffData { data: hex_bytes("03adb8de4bfb65db2cfd6120d55c6526ae9c52e675db7e47308636534ba7").unwrap() })).to_error_string()).into(), RuntimeCheckErrorKind::IncorrectArgumentCount(3, 2).into(), RuntimeCheckErrorKind::IncorrectArgumentCount(1, 2).into(), @@ -746,19 +743,30 @@ fn test_simple_if_functions(#[case] version: ClarityVersion, #[case] epoch: Stac .insert("without_else".into(), user_function2); let mut call_stack = CallStack::new(); - let mut env = Environment::new( - &mut global_context, - &contract_context, - &mut call_stack, - None, - None, - None, - ); + let mut exec_state = ExecutionState { + global_context: &mut global_context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { + contract_context: &contract_context, + sender: None, + caller: None, + sponsor: None, + }; if let Ok(tests) = evals { - assert_eq!(Ok(Value::Int(1)), eval(&tests[0], &mut env, &context)); - assert_eq!(Ok(Value::Int(3)), eval(&tests[1], &mut env, &context)); - assert_eq!(Ok(Value::Int(0)), eval(&tests[2], &mut env, &context)); + assert_eq!( + Ok(ValueRef::Owned(Value::Int(1))), + eval(&tests[0], &mut exec_state, &invoke_ctx, &context) + ); + assert_eq!( + Ok(ValueRef::Owned(Value::Int(3))), + eval(&tests[1], &mut exec_state, &invoke_ctx, &context) + ); + assert_eq!( + Ok(ValueRef::Owned(Value::Int(0))), + eval(&tests[2], &mut exec_state, &invoke_ctx, &context) + ); } else { panic!("Failed to parse function bodies."); } @@ -921,38 +929,34 @@ fn test_sequence_comparisons_clarity1() { let error_expectations: &[ClarityEvalError] = &[ RuntimeCheckErrorKind::UnionTypeValueError( vec![TypeSignature::IntType, TypeSignature::UIntType], - Box::new(Value::Sequence(SequenceData::String(CharType::ASCII( - ASCIIData { - data: "baa".as_bytes().to_vec(), - }, - )))), + Value::Sequence(SequenceData::String(CharType::ASCII(ASCIIData { + data: "baa".as_bytes().to_vec(), + }))) + .to_error_string(), ) .into(), RuntimeCheckErrorKind::UnionTypeValueError( vec![TypeSignature::IntType, TypeSignature::UIntType], - Box::new(Value::Sequence(SequenceData::String(CharType::ASCII( - ASCIIData { - data: "baa".as_bytes().to_vec(), - }, - )))), + Value::Sequence(SequenceData::String(CharType::ASCII(ASCIIData { + data: "baa".as_bytes().to_vec(), + }))) + .to_error_string(), ) .into(), RuntimeCheckErrorKind::UnionTypeValueError( vec![TypeSignature::IntType, TypeSignature::UIntType], - Box::new(Value::Sequence(SequenceData::String(CharType::ASCII( - ASCIIData { - data: "baa".as_bytes().to_vec(), - }, - )))), + Value::Sequence(SequenceData::String(CharType::ASCII(ASCIIData { + data: "baa".as_bytes().to_vec(), + }))) + .to_error_string(), ) .into(), RuntimeCheckErrorKind::UnionTypeValueError( vec![TypeSignature::IntType, TypeSignature::UIntType], - Box::new(Value::Sequence(SequenceData::String(CharType::ASCII( - ASCIIData { - data: "baa".as_bytes().to_vec(), - }, - )))), + Value::Sequence(SequenceData::String(CharType::ASCII(ASCIIData { + data: "baa".as_bytes().to_vec(), + }))) + .to_error_string(), ) .into(), ]; @@ -1032,12 +1036,12 @@ fn test_sequence_comparisons_mismatched_types() { let v1_error_expectations: &[ClarityEvalError] = &[ RuntimeCheckErrorKind::UnionTypeValueError( vec![TypeSignature::IntType, TypeSignature::UIntType], - Box::new(Value::Int(0)), + Value::Int(0).to_error_string(), ) .into(), RuntimeCheckErrorKind::UnionTypeValueError( vec![TypeSignature::IntType, TypeSignature::UIntType], - Box::new(Value::Int(0)), + Value::Int(0).to_error_string(), ) .into(), ]; @@ -1059,7 +1063,7 @@ fn test_sequence_comparisons_mismatched_types() { TypeSignature::STRING_UTF8_MAX, TypeSignature::BUFFER_MAX, ], - Box::new(Value::Int(0)), + Value::Int(0).to_error_string(), ) .into(), RuntimeCheckErrorKind::UnionTypeValueError( @@ -1070,7 +1074,7 @@ fn test_sequence_comparisons_mismatched_types() { TypeSignature::STRING_UTF8_MAX, TypeSignature::BUFFER_MAX, ], - Box::new(Value::Int(0)), + Value::Int(0).to_error_string(), ) .into(), ]; @@ -1093,11 +1097,10 @@ fn test_sequence_comparisons_mismatched_types() { TypeSignature::STRING_UTF8_MAX, TypeSignature::BUFFER_MAX, ], - Box::new(Value::Sequence(SequenceData::String(CharType::ASCII( - ASCIIData { - data: "baa".as_bytes().to_vec(), - }, - )))), + Value::Sequence(SequenceData::String(CharType::ASCII(ASCIIData { + data: "baa".as_bytes().to_vec(), + }))) + .to_error_string(), ) .into(), RuntimeCheckErrorKind::UnionTypeValueError( @@ -1108,11 +1111,10 @@ fn test_sequence_comparisons_mismatched_types() { TypeSignature::STRING_UTF8_MAX, TypeSignature::BUFFER_MAX, ], - Box::new(Value::Sequence(SequenceData::String(CharType::ASCII( - ASCIIData { - data: "baa".as_bytes().to_vec(), - }, - )))), + Value::Sequence(SequenceData::String(CharType::ASCII(ASCIIData { + data: "baa".as_bytes().to_vec(), + }))) + .to_error_string(), ) .into(), ]; @@ -1157,7 +1159,7 @@ fn test_simple_arithmetic_errors(#[case] version: ClarityVersion, #[case] epoch: RuntimeCheckErrorKind::IncorrectArgumentCount(2, 1).into(), RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::IntType), - Box::new(Value::Bool(true)), + Value::Bool(true).to_error_string(), ) .into(), RuntimeError::DivisionByZero.into(), @@ -1209,12 +1211,12 @@ fn test_unsigned_arithmetic() { RuntimeError::ArithmeticUnderflow.into(), RuntimeCheckErrorKind::UnionTypeValueError( vec![TypeSignature::IntType, TypeSignature::UIntType], - Box::new(Value::UInt(10)), + Value::UInt(10).to_error_string(), ) .into(), RuntimeCheckErrorKind::TypeValueError( Box::new(TypeSignature::UIntType), - Box::new(Value::Int(80)), + Value::Int(80).to_error_string(), ) .into(), RuntimeError::ArithmeticUnderflow.into(), @@ -1558,7 +1560,7 @@ fn test_hash_errors() { TypeSignature::UIntType, TypeSignature::BUFFER_MAX, ], - Box::new(Value::Bool(true)), + Value::Bool(true).to_error_string(), ) .into(), RuntimeCheckErrorKind::UnionTypeValueError( @@ -1567,7 +1569,7 @@ fn test_hash_errors() { TypeSignature::UIntType, TypeSignature::BUFFER_MAX, ], - Box::new(Value::Bool(true)), + Value::Bool(true).to_error_string(), ) .into(), RuntimeCheckErrorKind::UnionTypeValueError( @@ -1576,7 +1578,7 @@ fn test_hash_errors() { TypeSignature::UIntType, TypeSignature::BUFFER_MAX, ], - Box::new(Value::Bool(true)), + Value::Bool(true).to_error_string(), ) .into(), RuntimeCheckErrorKind::UnionTypeValueError( @@ -1585,7 +1587,7 @@ fn test_hash_errors() { TypeSignature::UIntType, TypeSignature::BUFFER_MAX, ], - Box::new(Value::Bool(true)), + Value::Bool(true).to_error_string(), ) .into(), RuntimeCheckErrorKind::IncorrectArgumentCount(1, 2).into(), @@ -1595,7 +1597,7 @@ fn test_hash_errors() { TypeSignature::UIntType, TypeSignature::BUFFER_MAX, ], - Box::new(Value::Bool(true)), + Value::Bool(true).to_error_string(), ) .into(), RuntimeCheckErrorKind::IncorrectArgumentCount(1, 2).into(), diff --git a/clarity/src/vm/tests/traits.rs b/clarity/src/vm/tests/traits.rs index 4dabfd4473b..c4cdd40a8ef 100644 --- a/clarity/src/vm/tests/traits.rs +++ b/clarity/src/vm/tests/traits.rs @@ -46,36 +46,43 @@ fn test_dynamic_dispatch_by_defining_trait( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-get-1", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-get-1", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(1)).unwrap() ); } @@ -102,36 +109,43 @@ fn test_dynamic_dispatch_pass_trait_nested_in_let( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-get-1", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-get-1", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(1)).unwrap() ); } @@ -157,36 +171,43 @@ fn test_dynamic_dispatch_pass_trait( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-get-1", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-get-1", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(1)).unwrap() ); } @@ -211,30 +232,36 @@ fn test_dynamic_dispatch_intra_contract_call( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("contract-defining-trait").unwrap(), - contract_defining_trait, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-defining-trait").unwrap(), + contract_defining_trait, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("dispatching-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); - let err_result = env + let err_result = exec_state .execute_contract( + &invoke_ctx, &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), "wrapped-get-1", &symbols_from_values(vec![target_contract]), @@ -268,41 +295,50 @@ fn test_dynamic_dispatch_by_implementing_imported_trait( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("contract-defining-trait").unwrap(), - contract_defining_trait, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-defining-trait").unwrap(), + contract_defining_trait, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-get-1", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-get-1", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(1)).unwrap() ); } @@ -330,41 +366,50 @@ fn test_dynamic_dispatch_by_implementing_imported_trait_mul_funcs( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("contract-defining-trait").unwrap(), - contract_defining_trait, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-defining-trait").unwrap(), + contract_defining_trait, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-get-1", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-get-1", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(1)).unwrap() ); } @@ -389,41 +434,50 @@ fn test_dynamic_dispatch_by_importing_trait( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("contract-defining-trait").unwrap(), - contract_defining_trait, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-defining-trait").unwrap(), + contract_defining_trait, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-get-1", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-get-1", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(1)).unwrap() ); } @@ -455,32 +509,43 @@ fn test_dynamic_dispatch_including_nested_trait( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("contract-defining-nested-trait").unwrap(), - contract_defining_nested_trait, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("contract-defining-trait").unwrap(), - contract_defining_trait, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-nested-contract").unwrap(), - target_nested_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-defining-nested-trait").unwrap(), + contract_defining_nested_trait, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-defining-trait").unwrap(), + contract_defining_trait, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-nested-contract").unwrap(), + target_nested_contract, + ) + .unwrap(); } { @@ -490,19 +555,21 @@ fn test_dynamic_dispatch_including_nested_trait( let target_nested_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-nested-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-get-1", - &symbols_from_values(vec![target_contract, target_nested_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-get-1", + &symbols_from_values(vec![target_contract, target_nested_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(99)).unwrap() ); } @@ -526,30 +593,36 @@ fn test_dynamic_dispatch_mismatched_args( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); - let err_result = env + let err_result = exec_state .execute_contract( + &invoke_ctx, &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), "wrapped-get-1", &symbols_from_values(vec![target_contract]), @@ -582,30 +655,36 @@ fn test_dynamic_dispatch_mismatched_returned( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); - let err_result = env + let err_result = exec_state .execute_contract( + &invoke_ctx, &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), "wrapped-get-1", &symbols_from_values(vec![target_contract]), @@ -639,30 +718,36 @@ fn test_reentrant_dynamic_dispatch( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); - let err_result = env + let err_result = exec_state .execute_contract( + &invoke_ctx, &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), "wrapped-get-1", &symbols_from_values(vec![target_contract]), @@ -694,30 +779,36 @@ fn test_readwrite_dynamic_dispatch( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); - let err_result = env + let err_result = exec_state .execute_contract( + &invoke_ctx, &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), "wrapped-get-1", &symbols_from_values(vec![target_contract]), @@ -752,30 +843,36 @@ fn test_readwrite_violation_dynamic_dispatch( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); - let err_result = env + let err_result = exec_state .execute_contract( + &invoke_ctx, &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), "wrapped-get-1", &symbols_from_values(vec![target_contract]), @@ -816,43 +913,54 @@ fn test_bad_call_with_trait( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("defun").unwrap(), - contract_defining_trait, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatch").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("implem").unwrap(), - impl_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("call").unwrap(), - caller_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("defun").unwrap(), + contract_defining_trait, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatch").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("implem").unwrap(), + impl_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("call").unwrap(), + caller_contract, + ) + .unwrap(); } { - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("call").unwrap(), - "foo-bar", - &symbols_from_values(vec![]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("call").unwrap(), + "foo-bar", + &symbols_from_values(vec![]), + false + ) + .unwrap(), Value::okay(Value::UInt(99)).unwrap() ); } @@ -880,43 +988,54 @@ fn test_good_call_with_trait( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("defun").unwrap(), - contract_defining_trait, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatch").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("implem").unwrap(), - impl_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("call").unwrap(), - caller_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("defun").unwrap(), + contract_defining_trait, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatch").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("implem").unwrap(), + impl_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("call").unwrap(), + caller_contract, + ) + .unwrap(); } { - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("call").unwrap(), - "foo-bar", - &symbols_from_values(vec![]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("call").unwrap(), + "foo-bar", + &symbols_from_values(vec![]), + false + ) + .unwrap(), Value::okay(Value::UInt(99)).unwrap() ); } @@ -945,47 +1064,58 @@ fn test_good_call_2_with_trait( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("defun").unwrap(), - contract_defining_trait, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatch").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("implem").unwrap(), - impl_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("call").unwrap(), - caller_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("defun").unwrap(), + contract_defining_trait, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatch").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("implem").unwrap(), + impl_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("call").unwrap(), + caller_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("implem").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("call").unwrap(), - "foo-bar", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("call").unwrap(), + "foo-bar", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(99)).unwrap() ); } @@ -1012,41 +1142,50 @@ fn test_dynamic_dispatch_pass_literal_principal_as_trait_in_user_defined_functio ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("contract-defining-trait").unwrap(), - contract_defining_trait, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("contract-defining-trait").unwrap(), + contract_defining_trait, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-get-1", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-get-1", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(1)).unwrap() ); } @@ -1072,22 +1211,29 @@ fn test_contract_of_value( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("defun").unwrap(), - contract_defining_trait, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatch").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("implem").unwrap(), - impl_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("defun").unwrap(), + contract_defining_trait, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatch").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("implem").unwrap(), + impl_contract, + ) + .unwrap(); } { @@ -1095,20 +1241,22 @@ fn test_contract_of_value( QualifiedContractIdentifier::local("implem").unwrap(), )); let result_contract = target_contract.clone(); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatch").unwrap(), - "wrapped-get-1", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatch").unwrap(), + "wrapped-get-1", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(result_contract).unwrap() ); } @@ -1136,22 +1284,29 @@ fn test_contract_of_no_impl( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("defun").unwrap(), - contract_defining_trait, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatch").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("implem").unwrap(), - impl_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("defun").unwrap(), + contract_defining_trait, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatch").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("implem").unwrap(), + impl_contract, + ) + .unwrap(); } { @@ -1159,20 +1314,22 @@ fn test_contract_of_no_impl( QualifiedContractIdentifier::local("implem").unwrap(), )); let result_contract = target_contract.clone(); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatch").unwrap(), - "wrapped-get-1", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatch").unwrap(), + "wrapped-get-1", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(result_contract).unwrap() ); } @@ -1198,36 +1355,43 @@ fn test_return_trait_with_contract_of_wrapped_in_begin( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-get-1", - &symbols_from_values(vec![target_contract.clone()]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-get-1", + &symbols_from_values(vec![target_contract.clone()]), + false + ) + .unwrap(), Value::okay(target_contract).unwrap() ); } @@ -1253,36 +1417,43 @@ fn test_return_trait_with_contract_of_wrapped_in_let( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-get-1", - &symbols_from_values(vec![target_contract.clone()]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-get-1", + &symbols_from_values(vec![target_contract.clone()]), + false + ) + .unwrap(), Value::okay(target_contract).unwrap() ); } @@ -1306,36 +1477,43 @@ fn test_return_trait_with_contract_of( ContractContext::new(QualifiedContractIdentifier::transient(), version); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-get-1", - &symbols_from_values(vec![target_contract.clone()]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-get-1", + &symbols_from_values(vec![target_contract.clone()]), + false + ) + .unwrap(), Value::okay(target_contract).unwrap() ); } @@ -1368,37 +1546,44 @@ fn test_pass_trait_to_subtrait(epoch: StacksEpochId, mut env_factory: MemoryEnvi ); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-get-1", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-get-1", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(1)).unwrap() ); } @@ -1428,18 +1613,23 @@ fn test_embedded_trait(epoch: StacksEpochId, mut env_factory: MemoryEnvironmentG ); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { @@ -1447,19 +1637,21 @@ fn test_embedded_trait(epoch: StacksEpochId, mut env_factory: MemoryEnvironmentG QualifiedContractIdentifier::local("target-contract").unwrap(), )); let opt_target = Value::some(target_contract).unwrap(); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "may-echo", - &symbols_from_values(vec![opt_target]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "may-echo", + &symbols_from_values(vec![opt_target]), + false + ) + .unwrap(), Value::okay(Value::UInt(42)).unwrap() ); } @@ -1499,37 +1691,44 @@ fn test_pass_embedded_trait_to_subtrait_optional( ); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-opt-get-1", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-opt-get-1", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(1)).unwrap() ); } @@ -1569,37 +1768,44 @@ fn test_pass_embedded_trait_to_subtrait_ok( ); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-ok-get-1", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-ok-get-1", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(1)).unwrap() ); } @@ -1639,37 +1845,44 @@ fn test_pass_embedded_trait_to_subtrait_err( ); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-err-get-1", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-err-get-1", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(1)).unwrap() ); } @@ -1709,37 +1922,44 @@ fn test_pass_embedded_trait_to_subtrait_list( ); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-list-get-1", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-list-get-1", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(1)).unwrap() ); } @@ -1782,37 +2002,44 @@ fn test_pass_embedded_trait_to_subtrait_list_option( ); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-list-get-1", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-list-get-1", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(1)).unwrap() ); } @@ -1855,37 +2082,44 @@ fn test_pass_embedded_trait_to_subtrait_option_list( ); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-list-get-1", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-list-get-1", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(1)).unwrap() ); } @@ -1914,37 +2148,44 @@ fn test_let_trait(epoch: StacksEpochId, mut env_factory: MemoryEnvironmentGenera ); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "let-echo", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "let-echo", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(42)).unwrap() ); } @@ -1977,37 +2218,44 @@ fn test_let3_trait(epoch: StacksEpochId, mut env_factory: MemoryEnvironmentGener ); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "let-echo", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "let-echo", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(42)).unwrap() ); } @@ -2036,37 +2284,44 @@ fn test_pass_principal_literal_to_trait( ); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract( - QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - dispatching_contract, - ) - .unwrap(); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + dispatching_contract, + ) + .unwrap(); - env.initialize_contract( - QualifiedContractIdentifier::local("target-contract").unwrap(), - target_contract, - ) - .unwrap(); + exec_state + .initialize_contract( + &invoke_ctx, + QualifiedContractIdentifier::local("target-contract").unwrap(), + target_contract, + ) + .unwrap(); } { let target_contract = Value::from(PrincipalData::Contract( QualifiedContractIdentifier::local("target-contract").unwrap(), )); - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert_eq!( - env.execute_contract( - &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), - "wrapped-get-1", - &symbols_from_values(vec![target_contract]), - false - ) - .unwrap(), + exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("dispatching-contract").unwrap(), + "wrapped-get-1", + &symbols_from_values(vec![target_contract]), + false + ) + .unwrap(), Value::okay(Value::UInt(1)).unwrap() ); } diff --git a/clarity/src/vm/tests/variables.rs b/clarity/src/vm/tests/variables.rs index 29b257d3084..aeea8c0b0ca 100644 --- a/clarity/src/vm/tests/variables.rs +++ b/clarity/src/vm/tests/variables.rs @@ -71,10 +71,11 @@ fn test_block_height( None, ); - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); // Call the function - let eval_result = env.eval_read_only(&contract_identifier, "(test-func)"); + let eval_result = exec_state.eval_read_only(&invoke_ctx, &contract_identifier, "(test-func)"); // In Clarity 3, this should trigger a runtime error if version >= ClarityVersion::Clarity3 { let err = eval_result.unwrap_err(); @@ -130,10 +131,11 @@ fn test_stacks_block_height( None, ); - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); // Call the function - let eval_result = env.eval_read_only(&contract_identifier, "(test-func)"); + let eval_result = exec_state.eval_read_only(&invoke_ctx, &contract_identifier, "(test-func)"); // In Clarity 3, this should trigger a runtime error if version < ClarityVersion::Clarity3 { let err = eval_result.unwrap_err(); @@ -191,10 +193,11 @@ fn test_tenure_height( None, ); - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); // Call the function - let eval_result = env.eval_read_only(&contract_identifier, "(test-func)"); + let eval_result = exec_state.eval_read_only(&invoke_ctx, &contract_identifier, "(test-func)"); // In Clarity 3, this should trigger a runtime error if version < ClarityVersion::Clarity3 { let err = eval_result.unwrap_err(); @@ -289,10 +292,11 @@ fn expect_contract_error( } } - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); // Call the function - let eval_result = env.eval_read_only(&contract_identifier, "(test-func)"); + let eval_result = exec_state.eval_read_only(&invoke_ctx, &contract_identifier, "(test-func)"); for (err_condition, expected_error) in expected_errors { if let ExpectedContractError::Runtime(expected_error) = expected_error { @@ -1205,10 +1209,11 @@ fn test_block_time( None, ); - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); // Call the function - let eval_result = env.eval_read_only(&contract_identifier, "(test-func)"); + let eval_result = exec_state.eval_read_only(&invoke_ctx, &contract_identifier, "(test-func)"); // In versions before Clarity 4, this should trigger a runtime error if version < ClarityVersion::Clarity4 { @@ -1257,20 +1262,24 @@ fn test_block_time_in_expressions() { ); assert!(result.is_ok()); - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); // Test comparison: 1 >= 0 should be true - let eval_result = env.eval_read_only(&contract_identifier, "(time-comparison u0)"); + let eval_result = + exec_state.eval_read_only(&invoke_ctx, &contract_identifier, "(time-comparison u0)"); info!("time-comparison result: {:?}", eval_result); assert_eq!(Ok(Value::Bool(true)), eval_result); // Test arithmetic: 1 + 100 = 101 - let eval_result = env.eval_read_only(&contract_identifier, "(time-arithmetic)"); + let eval_result = + exec_state.eval_read_only(&invoke_ctx, &contract_identifier, "(time-arithmetic)"); info!("time-arithmetic result: {:?}", eval_result); assert_eq!(Ok(Value::UInt(101)), eval_result); // Test in response: (ok 1) - let eval_result = env.eval_read_only(&contract_identifier, "(time-in-response)"); + let eval_result = + exec_state.eval_read_only(&invoke_ctx, &contract_identifier, "(time-in-response)"); info!("time-in-response result: {:?}", eval_result); assert_eq!(Ok(Value::okay(Value::UInt(1)).unwrap()), eval_result); } @@ -1334,10 +1343,11 @@ fn test_current_contract( None, ); - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); // Call the function - let eval_result = env.eval_read_only(&contract_identifier, "(test-func)"); + let eval_result = exec_state.eval_read_only(&invoke_ctx, &contract_identifier, "(test-func)"); // In Clarity 3, this should trigger a runtime error if version < ClarityVersion::Clarity4 { let err = eval_result.unwrap_err(); diff --git a/clarity/src/vm/variables.rs b/clarity/src/vm/variables.rs index 7b86b09cde5..42e476b2939 100644 --- a/clarity/src/vm/variables.rs +++ b/clarity/src/vm/variables.rs @@ -19,7 +19,7 @@ use stacks_common::types::StacksEpochId; use super::errors::VmInternalError; use crate::vm::ClarityVersion; -use crate::vm::contexts::{Environment, LocalContext}; +use crate::vm::contexts::{ExecutionState, InvocationContext}; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::runtime_cost; use crate::vm::errors::{RuntimeError, VmExecutionError}; @@ -50,27 +50,34 @@ pub fn is_reserved_name(name: &str, version: &ClarityVersion) -> bool { pub fn lookup_reserved_variable( name: &str, - _context: &LocalContext, - env: &mut Environment, + exec_state: &mut ExecutionState, + invoke_ctx: &InvocationContext, ) -> Result, VmExecutionError> { - if let Some(variable) = - NativeVariables::lookup_by_name_at_version(name, env.contract_context.get_clarity_version()) - { + if let Some(variable) = NativeVariables::lookup_by_name_at_version( + name, + invoke_ctx.contract_context.get_clarity_version(), + ) { match variable { NativeVariables::TxSender => { // This `NoSenderInContext` is **unreachable** in standard Clarity VM execution. // - Every function call (public, private, or trait) is executed with a valid caller context. - let sender = env.sender.clone().ok_or(RuntimeError::NoSenderInContext)?; + let sender = invoke_ctx + .sender + .clone() + .ok_or(RuntimeError::NoSenderInContext)?; Ok(Some(Value::Principal(sender))) } NativeVariables::ContractCaller => { // This `NoCallerInContext` is **unreachable** in standard Clarity VM execution. // - Every on-chain transaction and contract-call has a well-defined sender. - let caller = env.caller.clone().ok_or(RuntimeError::NoCallerInContext)?; + let caller = invoke_ctx + .caller + .clone() + .ok_or(RuntimeError::NoCallerInContext)?; Ok(Some(Value::Principal(caller))) } NativeVariables::TxSponsor => { - let sponsor = match env.sponsor.clone() { + let sponsor = match invoke_ctx.sponsor.clone() { None => Value::none(), Some(p) => Value::some(Value::Principal(p)).map_err(|_| { VmInternalError::Expect( @@ -81,24 +88,27 @@ pub fn lookup_reserved_variable( Ok(Some(sponsor)) } NativeVariables::BlockHeight => { - runtime_cost(ClarityCostFunction::FetchVar, env, 1)?; + runtime_cost(ClarityCostFunction::FetchVar, exec_state, 1)?; // In epoch 2.x, the `block-height` keyword returns the Stacks block height. // For Clarity 1 and Clarity 2 contracts executing in epoch 3, `block-height` // is equal to the tenure height instead of the Stacks block height. This change // is made to maintain a similar pace at which this value increments (e.g. for use // as an expiration). In Clarity 3, `block-height` is removed to avoid confusion. // It is replaced with two new keywords: `stacks-block-height` and `tenure-height`. - if env.global_context.epoch_id < StacksEpochId::Epoch30 { - let block_height = env.global_context.database.get_current_block_height(); + if exec_state.global_context.epoch_id < StacksEpochId::Epoch30 { + let block_height = exec_state + .global_context + .database + .get_current_block_height(); Ok(Some(Value::UInt(block_height as u128))) } else { - let tenure_height = env.global_context.database.get_tenure_height()?; + let tenure_height = exec_state.global_context.database.get_tenure_height()?; Ok(Some(Value::UInt(tenure_height as u128))) } } NativeVariables::BurnBlockHeight => { - runtime_cost(ClarityCostFunction::FetchVar, env, 1)?; - let burn_block_height = env + runtime_cost(ClarityCostFunction::FetchVar, exec_state, 1)?; + let burn_block_height = exec_state .global_context .database .get_current_burnchain_block_height()?; @@ -108,39 +118,45 @@ pub fn lookup_reserved_variable( NativeVariables::NativeTrue => Ok(Some(Value::Bool(true))), NativeVariables::NativeFalse => Ok(Some(Value::Bool(false))), NativeVariables::TotalLiquidMicroSTX => { - runtime_cost(ClarityCostFunction::FetchVar, env, 1)?; - let liq = env.global_context.database.get_total_liquid_ustx()?; + runtime_cost(ClarityCostFunction::FetchVar, exec_state, 1)?; + let liq = exec_state.global_context.database.get_total_liquid_ustx()?; Ok(Some(Value::UInt(liq))) } NativeVariables::Regtest => { - let reg = env.global_context.database.is_in_regtest(); + let reg = exec_state.global_context.database.is_in_regtest(); Ok(Some(Value::Bool(reg))) } NativeVariables::Mainnet => { - let mainnet = env.global_context.mainnet; + let mainnet = exec_state.global_context.mainnet; Ok(Some(Value::Bool(mainnet))) } NativeVariables::ChainId => { - let chain_id = env.global_context.chain_id; + let chain_id = exec_state.global_context.chain_id; Ok(Some(Value::UInt(chain_id.into()))) } NativeVariables::StacksBlockHeight => { - runtime_cost(ClarityCostFunction::FetchVar, env, 1)?; - let block_height = env.global_context.database.get_current_block_height(); + runtime_cost(ClarityCostFunction::FetchVar, exec_state, 1)?; + let block_height = exec_state + .global_context + .database + .get_current_block_height(); Ok(Some(Value::UInt(block_height as u128))) } NativeVariables::TenureHeight => { - runtime_cost(ClarityCostFunction::FetchVar, env, 1)?; - let tenure_height = env.global_context.database.get_tenure_height()?; + runtime_cost(ClarityCostFunction::FetchVar, exec_state, 1)?; + let tenure_height = exec_state.global_context.database.get_tenure_height()?; Ok(Some(Value::UInt(tenure_height as u128))) } NativeVariables::CurrentContract => { - let contract = env.contract_context.contract_identifier.clone(); + let contract = invoke_ctx.contract_context.contract_identifier.clone(); Ok(Some(Value::Principal(PrincipalData::Contract(contract)))) } NativeVariables::StacksBlockTime => { - runtime_cost(ClarityCostFunction::FetchVar, env, 1)?; - let block_time = env.global_context.database.get_current_block_time()?; + runtime_cost(ClarityCostFunction::FetchVar, exec_state, 1)?; + let block_time = exec_state + .global_context + .database + .get_current_block_time()?; Ok(Some(Value::UInt(u128::from(block_time)))) } } @@ -173,17 +189,18 @@ mod test { LimitedCostTracker::new_free(), StacksEpochId::Epoch2_05, ); - let mut env = Environment { + let mut exec_state = ExecutionState { + global_context: &mut global_context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { contract_context: &contract_context, sender: Some(PrincipalData::Standard(contract.issuer.clone())), caller: None, // <- intentionally missing sponsor: None, - global_context: &mut global_context, - call_stack: &mut call_stack, }; - let ctx = LocalContext::default(); - let res = lookup_reserved_variable("contract-caller", &ctx, &mut env); + let res = lookup_reserved_variable("contract-caller", &mut exec_state, &invoke_ctx); assert!(matches!( res, Err(VmExecutionError::Runtime( @@ -206,17 +223,17 @@ mod test { LimitedCostTracker::new_free(), StacksEpochId::Epoch2_05, ); - let mut env = Environment { + let mut exec_state = ExecutionState { + global_context: &mut global_context, + call_stack: &mut call_stack, + }; + let invoke_ctx = InvocationContext { contract_context: &contract_context, - caller: Some(PrincipalData::Standard(contract.issuer.clone())), sender: None, // <- intentionally missing + caller: Some(PrincipalData::Standard(contract.issuer.clone())), sponsor: None, - global_context: &mut global_context, - call_stack: &mut call_stack, }; - let ctx = LocalContext::default(); - - let res = lookup_reserved_variable("tx-sender", &ctx, &mut env); + let res = lookup_reserved_variable("tx-sender", &mut exec_state, &invoke_ctx); assert!(matches!( res, Err(VmExecutionError::Runtime( diff --git a/clarity/src/vm/version.rs b/clarity/src/vm/version.rs index 71c54233178..c7a94a57460 100644 --- a/clarity/src/vm/version.rs +++ b/clarity/src/vm/version.rs @@ -40,7 +40,7 @@ impl fmt::Display for ClarityVersion { impl ClarityVersion { pub const fn latest() -> ClarityVersion { - ClarityVersion::Clarity4 + ClarityVersion::Clarity5 } pub const ALL: &'static [ClarityVersion] = &[ diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 3a71ca00c59..00000000000 --- a/codecov.yml +++ /dev/null @@ -1,22 +0,0 @@ -# https://docs.codecov.com/docs/codecovyml-reference -codecov: - require_ci_to_pass: false - notify: - wait_for_ci: true -coverage: - range: 60..79 - round: down - precision: 2 - status: - changes: false - patch: false - project: - default: - target: auto - threshold: 0% -comment: - layout: "condensed_header, diff, files, footer" - hide_project_coverage: false - after_n_builds: 35 -github_checks: - annotations: false diff --git a/contrib/assemble-changelog.sh b/contrib/assemble-changelog.sh new file mode 100755 index 00000000000..53694c48afb --- /dev/null +++ b/contrib/assemble-changelog.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# +# Assemble changelog fragments into CHANGELOG.md for both stacks-node +# and stacks-signer. +# +# Usage: +# ./contrib/assemble-changelog.sh # both node and signer +# ./contrib/assemble-changelog.sh --signer # signer only +# +# By default, assembles both changelogs. The signer version is derived by +# appending ".0" to the node version (e.g., 3.3.0.0.7 -> 3.3.0.0.7.0). +# Use --signer for signer-only releases (version is used as-is for signer). +# +# The new version section is inserted before the first existing ## version +# header in each CHANGELOG.md. Fragment files are deleted after assembly. +# If a changelog directory has no fragments, it is skipped. +# +# Examples: +# ./contrib/assemble-changelog.sh 3.3.0.0.7 # node [3.3.0.0.7] + signer [3.3.0.0.7.0] +# ./contrib/assemble-changelog.sh 3.3.0.0.7.1 --signer # signer [3.3.0.0.7.1] only + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +if [ $# -lt 1 ]; then + echo "Usage: $0 [--signer]" >&2 + exit 1 +fi + +VERSION="$1" +shift + +SIGNER_ONLY=false +while [ $# -gt 0 ]; do + case "$1" in + --signer) SIGNER_ONLY=true; shift ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +shopt -s nullglob + +# assemble_changelog +assemble_changelog() { + local fragment_dir="$1" + local changelog="$2" + local version="$3" + + # --- Collect fragments by category --- + local -a ADDED=() + local -a CHANGED=() + local -a FIXED=() + local -a REMOVED=() + local found_any=false + + for ext in added changed fixed removed; do + for f in "$fragment_dir"/*."$ext"; do + [ -f "$f" ] || continue + found_any=true + while IFS= read -r line || [ -n "$line" ]; do + [ -z "$line" ] && continue + if [[ "$line" != "- "* ]]; then + line="- $line" + fi + case "$ext" in + added) ADDED+=("$line") ;; + changed) CHANGED+=("$line") ;; + fixed) FIXED+=("$line") ;; + removed) REMOVED+=("$line") ;; + esac + done < "$f" + done + done + + if [ "$found_any" = false ]; then + echo " No fragments found in $fragment_dir — skipping." + return + fi + + # --- Build the new section --- + local new_section="## [$version]" + + for category_name in Added Changed Fixed Removed; do + local -a entries=() + case "$category_name" in + Added) entries=("${ADDED[@]+"${ADDED[@]}"}") ;; + Changed) entries=("${CHANGED[@]+"${CHANGED[@]}"}") ;; + Fixed) entries=("${FIXED[@]+"${FIXED[@]}"}") ;; + Removed) entries=("${REMOVED[@]+"${REMOVED[@]}"}") ;; + esac + + if [ ${#entries[@]} -gt 0 ] && [ -n "${entries[0]}" ]; then + new_section+=$'\n\n'"### $category_name"$'\n' + for entry in "${entries[@]}"; do + new_section+=$'\n'"$entry" + done + fi + done + + # --- Insert into CHANGELOG.md --- + local section_file + section_file=$(mktemp) + printf '%s\n' "$new_section" > "$section_file" + + local tmpfile + tmpfile=$(mktemp) + + awk -v sfile="$section_file" ' + !inserted && /^## \[/ { + while ((getline sline < sfile) > 0) print sline + close(sfile) + print "" + inserted = 1 + } + { print } + ' "$changelog" > "$tmpfile" + + mv "$tmpfile" "$changelog" + rm -f "$section_file" + + # --- Delete assembled fragments --- + for ext in added changed fixed removed; do + for f in "$fragment_dir"/*."$ext"; do + [ -f "$f" ] || continue + rm "$f" + done + done + + echo " Assembled [$version] into $changelog" +} + +if [ "$SIGNER_ONLY" = true ]; then + echo "Assembling stacks-signer changelog..." + assemble_changelog "$REPO_ROOT/stacks-signer/changelog.d" "$REPO_ROOT/stacks-signer/CHANGELOG.md" "$VERSION" +else + echo "Assembling stacks-node changelog..." + assemble_changelog "$REPO_ROOT/changelog.d" "$REPO_ROOT/CHANGELOG.md" "$VERSION" + + echo "Assembling stacks-signer changelog..." + assemble_changelog "$REPO_ROOT/stacks-signer/changelog.d" "$REPO_ROOT/stacks-signer/CHANGELOG.md" "${VERSION}.0" +fi + +echo "Done." diff --git a/contrib/clarity-cli/README.md b/contrib/clarity-cli/README.md index ed86dea6935..658a1f223e7 100644 --- a/contrib/clarity-cli/README.md +++ b/contrib/clarity-cli/README.md @@ -38,7 +38,7 @@ clarity-cli initialize [OPTIONS] [ALLOCATIONS_FILE] **Options:** - `--testnet` - Use testnet boot code and block limits (default: mainnet) -- `--epoch ` - Stacks epoch to use (default: 3.3) +- `--epoch ` - Stacks epoch to use (default: 3.4) **Example:** ```bash @@ -91,7 +91,7 @@ clarity-cli check [OPTIONS] [DB_PATH] - `--output-analysis` - Include contract interface analysis in output - `--costs` - Include execution costs in output - `--testnet` - Use testnet configuration -- `--clarity-version ` - Clarity version (e.g., `clarity1`, `clarity2`, `clarity3`, `clarity4`) +- `--clarity-version ` - Clarity version (e.g., `clarity1`, `clarity2`, `clarity3`, `clarity4`, `clarity5`) - `--epoch ` - Stacks epoch (e.g., `2.1`, `2.5`, `3.0`) **Example:** @@ -323,11 +323,11 @@ echo "(contract-call? .my-contract get-value)" | \ ## Epoch and Clarity Version -The CLI defaults to Epoch 3.3 with Clarity 4. You can specify earlier epochs/versions for compatibility testing. +The CLI defaults to Epoch 3.4 with Clarity 5. You can specify earlier epochs/versions for compatibility testing. -**Valid epoch values:** `1.0`, `2.0`, `2.05`, `2.1`, `2.2`, `2.3`, `2.4`, `2.5`, `3.0`, `3.1`, `3.2`, `3.3` +**Valid epoch values:** `1.0`, `2.0`, `2.05`, `2.1`, `2.2`, `2.3`, `2.4`, `2.5`, `3.0`, `3.1`, `3.2`, `3.3`, `3.4` -**Valid clarity version values:** `clarity1`, `clarity2`, `clarity3`, `clarity4` +**Valid clarity version values:** `clarity1`, `clarity2`, `clarity3`, `clarity4`, `clarity5` | Epoch | Default Clarity Version | |-------|------------------------| @@ -342,6 +342,7 @@ The CLI defaults to Epoch 3.3 with Clarity 4. You can specify earlier epochs/ver | 3.1 | Clarity 3 | | 3.2 | Clarity 3 | | 3.3 | Clarity 4 | +| 3.4 | Clarity 5 | See `clarity/src/vm/version.rs` for Clarity version definitions and `stacks-common/src/types/mod.rs` for epoch definitions. diff --git a/contrib/clarity-cli/src/lib.rs b/contrib/clarity-cli/src/lib.rs index a45bebc7922..e6e58bda77d 100644 --- a/contrib/clarity-cli/src/lib.rs +++ b/contrib/clarity-cli/src/lib.rs @@ -780,7 +780,7 @@ fn install_boot_code( QualifiedContractIdentifier::transient().issuer.into(), None, None, - |env| { + |env, _invoke_ctx| { let res: Result<_, VmExecutionError> = Ok(env.global_context.database.set_clarity_epoch_version(epoch)); res @@ -1072,7 +1072,8 @@ pub fn execute_repl( ); let placeholder_context = ContractContext::new(QualifiedContractIdentifier::transient(), clarity_version); - let mut exec_env = vm_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + vm_env.get_exec_environment(None, None, &placeholder_context); let mut analysis_marf = MemoryBackingStore::new(); let contract_id = QualifiedContractIdentifier::transient(); @@ -1125,7 +1126,7 @@ pub fn execute_repl( } // Evaluate the expression - let eval_result = match exec_env.eval_raw(&content) { + let eval_result = match exec_state.eval_raw(&invoke_ctx, &content) { Ok(val) => val, Err(error) => { println!("Execution error:\n{error}"); @@ -1174,9 +1175,9 @@ pub fn execute_eval_raw( ) { Ok(_) => { // Analysis passed, now evaluate - let result = vm_env - .get_exec_environment(None, None, &placeholder_context) - .eval_raw(content); + let (mut exec_state, invoke_ctx) = + vm_env.get_exec_environment(None, None, &placeholder_context); + let result = exec_state.eval_raw(&invoke_ctx, content); match result { Ok(x) => ( 0, @@ -1231,9 +1232,9 @@ pub fn execute_eval( // Evaluate in a new block let (_, _, result_and_cost) = in_block(header_db, marf_kv, |header_db, mut marf| { let result_and_cost = with_env_costs(mainnet, epoch, &header_db, &mut marf, |vm_env| { - vm_env - .get_exec_environment(None, None, &placeholder_context) - .eval_read_only(contract_identifier, content) + let (mut exec_state, invoke_ctx) = + vm_env.get_exec_environment(None, None, &placeholder_context); + exec_state.eval_read_only(&invoke_ctx, contract_identifier, content) }); (header_db, marf, result_and_cost) }); @@ -1290,9 +1291,9 @@ pub fn execute_eval_at_chaintip( // Evaluate at chaintip (no block advance) let result_and_cost = at_chaintip(vm_filename, marf_kv, |mut marf| { let result_and_cost = with_env_costs(mainnet, epoch, &header_db, &mut marf, |vm_env| { - vm_env - .get_exec_environment(None, None, &placeholder_context) - .eval_read_only(contract_identifier, content) + let (mut exec_state, invoke_ctx) = + vm_env.get_exec_environment(None, None, &placeholder_context); + exec_state.eval_read_only(&invoke_ctx, contract_identifier, content) }); let (result, cost) = result_and_cost; @@ -1351,9 +1352,9 @@ pub fn execute_eval_at_block( // Evaluate at specific block let result_and_cost = at_block(chain_tip, marf_kv, |mut marf| { let result_and_cost = with_env_costs(mainnet, epoch, &header_db, &mut marf, |vm_env| { - vm_env - .get_exec_environment(None, None, &placeholder_context) - .eval_read_only(contract_identifier, content) + let (mut exec_state, invoke_ctx) = + vm_env.get_exec_environment(None, None, &placeholder_context); + exec_state.eval_read_only(&invoke_ctx, contract_identifier, content) }); (marf, result_and_cost) }); diff --git a/contrib/init/stacks.service b/contrib/init/stacks.service index 57b0af81e3a..d90012cf350 100644 --- a/contrib/init/stacks.service +++ b/contrib/init/stacks.service @@ -21,7 +21,8 @@ ExecStartPre=/bin/chgrp stacks /etc/stacks-blockchain/ # Process management #################### PIDFile=/run/stacks-blockchain/stacks-blockchain.pid -Restart=no +Restart=on-failure +RestartSec=30 TimeoutStopSec=600 KillSignal=SIGINT SendSIGKILL=no diff --git a/docs/follower.md b/docs/follower.md new file mode 100644 index 00000000000..a299e4677b1 --- /dev/null +++ b/docs/follower.md @@ -0,0 +1,107 @@ +# Running a Stacks Follower Node + +A follower (or "full node") syncs the Stacks blockchain without mining or signing. +Use cases include serving RPC/API requests, running a stacks-blockchain-api instance, +or monitoring the chain. + +## Quick Start + +```toml +[node] +working_dir = "/stacks-data/mainnet" +rpc_bind = "0.0.0.0:20443" +p2p_bind = "0.0.0.0:20444" +miner = false +stacker = false + +[burnchain] +mode = "mainnet" +peer_host = "127.0.0.1" +``` + +Start the node: + +```bash +stacks-node start --config=mainnet-follower-conf.toml +``` + +## Bitcoin Node + +A follower needs a Bitcoin node to sync burnchain data. For mainnet, the default +ports (`rpc_port = 8332`, `peer_port = 8333`) are used. If your Bitcoin node requires +RPC authentication, add credentials to `[burnchain]`: + +```toml +[burnchain] +username = "your-bitcoin-rpc-user" +password = "your-bitcoin-rpc-password" +``` + +## API Integration + +To run a stacks-blockchain-api service alongside the follower, enable the events +observer and transaction indexing: + +```toml +[node] +txindex = true + +[[events_observer]] +endpoint = "localhost:3700" +events_keys = ["*"] +timeout_ms = 60_000 +``` + +## Upgrading to a Signer Node + +A follower can be upgraded to also serve a signer by adding three settings. +See [signing.md](signing.md) for full details. + +```toml +[node] +stacker = true + +[[events_observer]] +endpoint = "127.0.0.1:30000" +events_keys = ["stackerdb", "block_proposal", "burn_blocks"] + +[connection_options] +auth_token = "your-secret-token" +``` + +## Local Development (Mocknet) + +For local development without a Bitcoin node, use mocknet mode: + +```bash +stacks-node start --config=mocknet.toml +``` + +Mocknet runs a simulated burnchain in-process, removes execution cost limits, +and requires pre-funded test accounts via `[[ustx_balance]]` entries. +See [`mocknet.toml`](../sample/conf/mocknet.toml). + +## Environment Variables + +These environment variables affect node behavior and cannot be set via TOML: + +| Variable | Purpose | +| --- | --- | +| `STACKS_EVENT_OBSERVER` | Add an event observer endpoint (all events) | +| `STACKS_WORKING_DIR` | Override `node.working_dir` | +| `STACKS_LOG_JSON` | Enable JSON-formatted logging | +| `STACKS_LOG_DEBUG` | Enable debug-level logging | +| `STACKS_LOG_TRACE` | Enable trace-level logging | + +## Configuration Files + +| File | Purpose | +| --- | --- | +| [`mainnet-follower-conf.toml`](../sample/conf/mainnet-follower-conf.toml) | Mainnet follower | +| [`testnet-follower-conf.toml`](../sample/conf/testnet-follower-conf.toml) | Testnet follower | +| [`mocknet.toml`](../sample/conf/mocknet.toml) | Local mocknet development | + +## Further Reading + +- [Mining documentation](mining.md) +- [Signing documentation](signing.md) diff --git a/docs/mining.md b/docs/mining.md index 320ab41cb08..ff558e33682 100644 --- a/docs/mining.md +++ b/docs/mining.md @@ -6,11 +6,11 @@ you should make sure to add the following config fields to your [config file](.. ```toml [node] # Run as a miner -miner = True +miner = true # Bitcoin private key to spend seed = "YOUR PRIVATE KEY" -# Run as a mock-miner, to test mining without spending BTC. Needs miner=True. -#mock_mining = True +# Enable stacker support (required for signer coordination) +stacker = true [miner] # Time to spend mining a Nakamoto block, in milliseconds. @@ -25,8 +25,45 @@ satoshis_per_byte = 50 rbf_fee_increment = 5 # Maximum percentage of satoshis_per_byte to allow in RBF fee (default: 150) max_rbf = 150 + +[connection_options] +# Must match signer's auth_password +auth_token = "your-secret-token" + +[[events_observer]] +# Must match signer's endpoint +endpoint = "127.0.0.1:30000" +events_keys = ["stackerdb", "block_proposal", "burn_blocks"] ``` +> To test mining without spending BTC, add `mock_mining = true` to the `[node]` +> section. See also [`mainnet-mockminer-conf.toml`](../sample/conf/mainnet-mockminer-conf.toml). + +For a comprehensive reference of **all** miner settings including signer coordination +timeouts, tenure management, mempool configuration, and cost limits, see +[`mainnet-miner-conf.toml`](../sample/conf/mainnet-miner-conf.toml). + +## Signer Setup + +Nakamoto mining requires a co-located signer. See [signing.md](signing.md) for +signer configuration and the critical miner-signer coordination settings. + +## Configuration Files + +| File | Purpose | +| ---------------------------------------------------------------------------- | ------------------------------------------------------- | +| [`mainnet-miner-conf.toml`](../sample/conf/mainnet-miner-conf.toml) | Comprehensive miner reference (all settings documented) | +| [`mainnet-signer-conf.toml`](../sample/conf/signer/mainnet-signer-conf.toml) | Signer binary config reference | +| [`mainnet-signer.toml`](../sample/conf/mainnet-signer.toml) | Node-side signer config | +| [`testnet-miner-conf.toml`](../sample/conf/testnet-miner-conf.toml) | Testnet miner config | +| [`mainnet-mockminer-conf.toml`](../sample/conf/mainnet-mockminer-conf.toml) | Mock miner (test mining without spending BTC) | +| [`mainnet-follower-conf.toml`](../sample/conf/mainnet-follower-conf.toml) | Mainnet follower (read-only node) | +| [`testnet-follower-conf.toml`](../sample/conf/testnet-follower-conf.toml) | Testnet follower | +| [`testnet-signer.toml`](../sample/conf/testnet-signer.toml) | Testnet node-side signer config | +| [`mocknet.toml`](../sample/conf/mocknet.toml) | Local mocknet development | + +## RBF Configuration + NOTE: Ensuring that your miner can successfully use RBF (Replace-by-Fee) is critical for reliable block production. If a miner fails to replace an outdated block commit with a higher-fee transaction, it risks committing to an incorrect @@ -81,5 +118,7 @@ Estimates are then randomly "fuzzed" using uniform random fuzz of size up to ## Further Reading +- [Signing documentation](signing.md) +- [Follower documentation](follower.md) - [stacksfoundation/miner-docs](https://github.com/stacksfoundation/miner-docs) - [Mining Documentation](https://docs.stacks.co/stacks-in-depth/nodes-and-miners/mine-mainnet-stacks-tokens) diff --git a/docs/property-testing.md b/docs/property-testing.md index 866aa13e3fa..eaea7d824dd 100644 --- a/docs/property-testing.md +++ b/docs/property-testing.md @@ -142,6 +142,7 @@ Finally, we can actually write the property test: ```rust proptest! { + #[tag(t_prop)] #[test] fn make_reward_set( pox_slots in 1..4_000u32, @@ -179,6 +180,7 @@ For the above example, one thing we really want to be sure of is that multiple e So to deal with this, we can alter our input generation so that we're getting more interesting test cases: ```rust + #[tag(t_prop)] #[test] fn make_reward_set( pox_slots in 1..4_000u32, @@ -201,6 +203,8 @@ So to deal with this, we can alter our input generation so that we're getting mo This technique allows to be sure that proptest generates a lot of cases where there are multiple entries for the same reward address. Unfortunately, this kind of thing tends to be more art than science, which means that PR authors and reviewers will need to be careful about the input strategies for property tests (this should also be aided by the CI task for PRs). This is one of the reasons that property tests can't totally supplant unit tests. However, a lot of the work of property tests helps with writing unit tests: many unit tests can be essentially fixed inputs to the property test. +> NOTE: As a requirement for CI automation, prop tests need to be tagged with `#[tag(t_prop)]`. + ## Reusing Strategies Writing new input strategies may be the most tedious part of writing property tests, so it is worthwhile figuring out if the input you are looking for (or maybe a component of the input you're looking for) already has a strategy in the codebase. If you search for functions that return `impl Strategy` in the codebase, you should find the set of functions that have already been written. @@ -211,8 +215,8 @@ Except in cases where input strategies are highly tailored to a particular test, By default, we'll get some CI integration from `proptest` automatically: the new property tests will run with 250 randomly generated inputs on every execution of the unit test job in CI. This is great. However, we want some additional support for executing *new* property tests extra amounts before PRs merge. -The environment variable `PROPTEST_CASES` can be set to a higher number (e.g., `PROPTEST_CASES=2500`) to explore more test cases before declaring success. From the CI, what we want is a job which: +The environment variable `PROPTEST_CASES` can be set to a higher number (e.g., `PROPTEST_CASES=2500`) to explore more test cases before declaring success. From the CI, we have then a job which: 1. Executes once a PR has been approved. -2. Discovers the set of new tests (this is probably easiest to achieve by running `cargo nextest list` on the source and target branches and then diffing the outputs). -3. Executes only the new tests with the environment variable `PROPTEST_CASES` set to 2500. +2. Discovers the set of new prop tests introduced (NOTE: is relevant for this stage that prop tests are tagged with `#[tag(t_prop)]`. See examples above). +3. Executes the new tests with the environment variable `PROPTEST_CASES` set to `2500`. diff --git a/docs/release-process.md b/docs/release-process.md index 0e87ce60135..64b7b535c76 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -66,17 +66,25 @@ The timing of the next Stacking cycle can be found [here](https://stx.eco/dao/to - Add cherry-picked commits to the `feat/X.Y.Z.A.n-pr_number` branch - Merge `feat/X.Y.Z.A.n-pr_number` into `release/X.Y.Z.A.n`. -5. If necessary, open a PR to update the [CHANGELOG](../CHANGELOG.md) in the `release/X.Y.Z.A.n` branch. +5. Open a PR to assemble the changelog and update versions in the `release/X.Y.Z.A.n` branch. - Create a chore branch from `release/X.Y.Z.A.n`, ex: `chore/X.Y.Z.A.n-changelog`. - Update [versions.toml](../versions.toml) to match this release: - Update the `stacks_node_version` string to match this release version. - Update the `stacks_signer_version` string to match `stacks_node_version`, with an appending `0` for this release version. - - Add summaries of all Pull Requests to the `Added`, `Changed` and `Fixed` sections. + - Assemble changelog fragments into `CHANGELOG.md` and `stacks-signer/CHANGELOG.md`: - - Pull requests merged into `develop` can be found [here](https://github.com/stacks-network/stacks-core/pulls?q=is%3Apr+is%3Aclosed+base%3Adevelop+sort%3Aupdated-desc). + ```bash + ./contrib/assemble-changelog.sh X.Y.Z.A.n + ``` - **Note**: GitHub does not allow sorting by _merge time_, so, when sorting by some proxy criterion, some care should be used to understand which PR's were _merged_ after the last release. + This will collect all fragment files from `changelog.d/` and `stacks-signer/changelog.d/`, + group them by category (Added/Changed/Fixed/Removed), insert them as a new version section + in the respective `CHANGELOG.md`, and delete the assembled fragments. For a signer-only + release, the flag `--signer` can be passed to only process the signer fragments and upate + `stacks-signer/CHANGELOG.md`. + + Review the assembled changelog for accuracy and make any manual adjustments if needed. - This PR must be merged before continuing to the next steps diff --git a/docs/rpc-endpoints.md b/docs/rpc-endpoints.md index 88e8e9d3b20..df70e4b269d 100644 --- a/docs/rpc-endpoints.md +++ b/docs/rpc-endpoints.md @@ -79,7 +79,9 @@ Reason types without additional information will not have a ### GET /v2/pox -Get current PoX-relevant information. See OpenAPI [spec](./rpc/openapi.yaml) for details. +Get current PoX-relevant information. See the [OpenAPI spec](./rpc/openapi.yaml) for details. + +This endpoint accepts a query string parameter `?tip=` to control which chain tip state is queried. ### GET /v2/headers/[Count] @@ -130,7 +132,7 @@ non-canonical headers will be returned instead. Get the account data for the provided principal. The principal string is either a Stacks address or a Contract identifier (e.g., -`SP31DA6FTSJX2WGTZ69SFY11BH51NZMB0ZW97B5P0.get-info` +`SP31DA6FTSJX2WGTZ69SFY11BH51NZMB0ZW97B5P0.get-info`) Returns JSON data in the form: @@ -150,14 +152,20 @@ provided as hex strings. For non-existent accounts, this _does not_ 404, rather it returns an object with balance and nonce of 0. -This endpoint also accepts a querystring parameter `?proof=` which when supplied `0`, will return the +This endpoint also accepts a query string parameter `?proof=` which when supplied `0`, will return the JSON object _without_ the `balance_proof` or `nonce_proof` fields. +This endpoint accepts a query string parameter `?tip=` to control which chain tip state is queried. +See the [OpenAPI spec](./rpc/openapi.yaml) `tip` parameter for details. + ### GET /v2/data_var/[Stacks Address]/[Contract Name]/[Var Name] -Attempt to vetch a data var from a contract. The contract is identified with [Stacks Address] and +Attempt to fetch a data var from a contract. The contract is identified with [Stacks Address] and [Contract Name] in the URL path. The variable is identified with [Var Name]. +This endpoint accepts a query string parameter `?tip=` to control which chain tip state is queried. +See the [OpenAPI spec](./rpc/openapi.yaml) `tip` parameter for details. + Returns JSON data in the form: ```json @@ -169,12 +177,16 @@ Returns JSON data in the form: Where data is the hex serialization of the variable value. -This endpoint also accepts a querystring parameter `?proof=` which when supplied `0`, will return the +This endpoint also accepts a query string parameter `?proof=` which when supplied `0`, will return the JSON object _without_ the `proof` field. ### GET /v2/clarity/marf/[Clarity MARF Key] Attempt to fetch the value of a MARF key. The key is identified with [Clarity MARF Key]. +This endpoint accepts query string parameters `?tip=` and `?proof=` to control which chain tip +state is queried and whether a MARF proof is included. +See the [OpenAPI spec](./rpc/openapi.yaml) for details. + Returns JSON data in the form: ```json @@ -191,6 +203,9 @@ Attempt to fetch the metadata of a contract. The contract is identified with [Stacks Address] and [Contract Name] in the URL path. The metadata key is identified with [Clarity Metadata Key]. +This endpoint accepts a query string parameter `?tip=` to control which chain tip state is queried. +See the [OpenAPI spec](./rpc/openapi.yaml) `tip` parameter for details. + Returns JSON data in the form: ```json @@ -205,6 +220,9 @@ Where data is the metadata formatted as a JSON string. Attempt to fetch a constant from a contract. The contract is identified with [Stacks Address] and [Contract Name] in the URL path. The constant is identified with [Constant Name]. +This endpoint accepts a query string parameter `?tip=` to control which chain tip state is queried. +See the [OpenAPI spec](./rpc/openapi.yaml) `tip` parameter for details. + Returns JSON data in the form: ```json @@ -223,6 +241,9 @@ Attempt to fetch data from a contract data map. The contract is identified with The _key_ to lookup in the map is supplied via the POST body. This should be supplied as the hex string serialization of the key (which should be a Clarity value). Note, this is a _JSON_ string atom. +This endpoint accepts a query string parameter `?tip=` to control which chain tip state is queried. +See the [OpenAPI spec](./rpc/openapi.yaml) `tip` parameter for details. + Returns JSON data in the form: ```json @@ -236,7 +257,7 @@ Where data is the hex serialization of the map response. Note that map responses for non-existent values, this is a serialized `none`, and for all other responses, it is a serialized `(some ...)` object. -This endpoint also accepts a querystring parameter `?proof=` which when supplied `0`, will return the +This endpoint also accepts a query string parameter `?proof=` which when supplied `0`, will return the JSON object _without_ the `proof` field. ### GET /v2/fees/transfer @@ -247,6 +268,9 @@ Get an estimated fee rate for STX transfer transactions. This is a fee rate / by Fetch the contract interface for a given contract, identified by [Stacks Address] and [Contract Name]. +This endpoint accepts a query string parameter `?tip=` to control which chain tip state is queried. +See the [OpenAPI spec](./rpc/openapi.yaml) `tip` parameter for details. + This returns a JSON object of the form: ```json @@ -399,6 +423,9 @@ This returns a JSON object of the form: Fetch the source for a smart contract, along with the block height it was published in, and the MARF proof for the data. +This endpoint accepts a query string parameter `?tip=` to control which chain tip state is queried. +See the [OpenAPI spec](./rpc/openapi.yaml) `tip` parameter for details. + ```json { "source": "(define-private ...", @@ -407,7 +434,7 @@ published in, and the MARF proof for the data. } ``` -This endpoint also accepts a querystring parameter `?proof=` which +This endpoint also accepts a query string parameter `?proof=` which when supplied `0`, will return the JSON object _without_ the `proof` field. @@ -416,7 +443,10 @@ field. Call a read-only public function on a given smart contract. The smart contract and function are specified using the URL path. The arguments and -the simulated `tx-sender` are supplied via the POST body in the following JSON format: +the simulated `tx-sender` are supplied via the POST body in the following JSON format. + +This endpoint accepts a query string parameter `?tip=` to control which chain tip state is queried. +See the [OpenAPI spec](./rpc/openapi.yaml) `tip` parameter for details. ```json { @@ -454,7 +484,8 @@ object of the following form: Determine whether a given trait is implemented within the specified contract (either explicitly or implicitly). -See OpenAPI [spec](./rpc/openapi.yaml) for details. +This endpoint accepts a query string parameter `?tip=` to control which chain tip state is queried. +See the [OpenAPI spec](./rpc/openapi.yaml) `tip` parameter for details. ### POST /v3/block_proposal @@ -539,7 +570,7 @@ data. This will return 404 if the block does not exist. -This endpoint also accepts a querystring parameter `?tip=` which when supplied +This endpoint also accepts a query string parameter `?tip=` which when supplied will return the block relative to the specified tip allowing the querying of sibling blocks (same height, different tip) too. @@ -575,7 +606,7 @@ tenure, `tip_block_id` identifies the highest-known block in this tenure, and ### GET /v3/signer/[Signer Pubkey]/[Reward Cycle] -Get number of blocks signed by signer during a given reward cycle +Get number of blocks signed by signer during a given reward cycle. Returns a non-negative integer diff --git a/docs/rpc/components/parameters/tip.yaml b/docs/rpc/components/parameters/tip.yaml index d6a53a06133..351b083f073 100644 --- a/docs/rpc/components/parameters/tip.yaml +++ b/docs/rpc/components/parameters/tip.yaml @@ -8,5 +8,11 @@ schema: description: | Stacks chain tip to query from. Options: - (empty/omitted): Use latest anchored tip (canonical confirmed state) - - `latest`: Use latest known tip including unconfirmed microblocks - - `{block_id}`: Use specific block ID (64 hex characters) + - `latest`: Use latest known tip including unconfirmed microblocks. + If no unconfirmed state is available, falls back to the confirmed canonical tip. + If the unconfirmed state check fails with an error, returns 404. + - `{block_id}`: Use specific block ID (64 lowercase hex characters) + + **Note:** If `tip` is present but contains an invalid or malformed value + (i.e., not `latest` and not a valid 64-character hex block ID), + the node silently falls back to the latest anchored tip (same as omitting `tip`). diff --git a/docs/rpc/components/schemas/read-only-function-args.schema.yaml b/docs/rpc/components/schemas/read-only-function-args.schema.yaml index ee4ad260802..d553a5739e1 100644 --- a/docs/rpc/components/schemas/read-only-function-args.schema.yaml +++ b/docs/rpc/components/schemas/read-only-function-args.schema.yaml @@ -1,4 +1,4 @@ -description: Describes representation of a Type-0 Stacks 2.0 transaction. https://github.com/stacksgov/sips/blob/main/sips/sip-005/sip-005-blocks-and-transactions.md#type-0-transferring-an-asset +description: Arguments for a simulated read-only Clarity function call, including the sender/sponsor principals and hex-encoded Clarity values. type: object additionalProperties: false required: @@ -6,9 +6,9 @@ required: - arguments properties: sender: - description: The simulated tx-sender. + description: The simulated tx-sender (standard address or contract principal). allOf: - - $ref: './standard-principal.schema.yaml' + - $ref: './principal.schema.yaml' sponsor: description: The simulated sponsor address. allOf: diff --git a/docs/rpc/openapi.yaml b/docs/rpc/openapi.yaml index 6997c1a9c61..b527f243f23 100644 --- a/docs/rpc/openapi.yaml +++ b/docs/rpc/openapi.yaml @@ -374,7 +374,7 @@ paths: The arguments to the function are supplied via the POST body. This should be a JSON object with two main properties: - - `sender` which should be a standard Stacks address + - `sender` which should be a Stacks address or contract principal - `arguments` which should be an array of hex-encoded Clarity values. tags: - Smart Contracts @@ -425,7 +425,7 @@ paths: The arguments to the function are supplied via the POST body. This should be a JSON object with two main properties: - - `sender` which should be a standard Stacks address + - `sender` which should be a Stacks address or contract principal - `arguments` which should be an array of hex-encoded Clarity values. **This API endpoint requires a basic Authorization header.** @@ -1444,7 +1444,6 @@ paths: schema: type: integer minimum: 0 - - $ref: ./components/parameters/tip.yaml responses: "200": description: Number of blocks signed @@ -1636,7 +1635,6 @@ paths: schema: type: string pattern: "^[0-9a-f]{64}$" - - $ref: ./components/parameters/tip.yaml responses: "200": description: Stream of confirmed microblocks diff --git a/docs/signing.md b/docs/signing.md new file mode 100644 index 00000000000..12bb6fae150 --- /dev/null +++ b/docs/signing.md @@ -0,0 +1,96 @@ +# Stacks Signing + +Stacks signers validate and co-sign blocks produced by miners. Running a signer +requires two configuration files: + +1. **Signer binary config** — configures the `stacks-signer` process +2. **Signer node config** — configures the `stacks-node` that the signer connects to + +## Configuration Files + +| File | Binary | Purpose | +| ---------------------------------------------------------------------------- | --------------- | ----------------------------------------------------------- | +| [`mainnet-signer-conf.toml`](../sample/conf/signer/mainnet-signer-conf.toml) | `stacks-signer` | Signer process settings (keys, timeouts, tenure management) | +| [`mainnet-signer.toml`](../sample/conf/mainnet-signer.toml) | `stacks-node` | Node-side settings (events, auth, networking) | + +For testnet, use [`testnet-signer.toml`](../sample/conf/testnet-signer.toml) for the node-side config. + +## Quick Start + +### 1. Configure the Stacks Node + +Use [`mainnet-signer.toml`](../sample/conf/mainnet-signer.toml) as a starting point for your node config. +Key settings: + +```toml +[node] +stacker = true + +[[events_observer]] +endpoint = "127.0.0.1:30000" +events_keys = ["stackerdb", "block_proposal", "burn_blocks"] + +[connection_options] +auth_token = "your-secret-token" +``` + +### 2. Configure the Signer + +Use [`mainnet-signer-conf.toml`](../sample/conf/signer/mainnet-signer-conf.toml) as a starting point. +Key settings: + +```toml +stacks_private_key = "" +node_host = "127.0.0.1:20443" +endpoint = "0.0.0.0:30000" +network = "mainnet" +auth_password = "your-secret-token" +db_path = "/var/lib/stacks-signer/signerdb.sqlite" +``` + +### 3. Verify Coordination + +These settings **must** match between the node and signer configs: + +| Signer Config | Node Config | Must Match | +| --------------- | --------------------------------- | ----------------------------- | +| `auth_password` | `[connection_options] auth_token` | Exact string match | +| `endpoint` | `[[events_observer]] endpoint` | Same host:port | +| `node_host` | `[node] rpc_bind` | Signer connects to node's RPC | + +## Miner-Signer Interactions + +If you are running both a miner and a signer, several timeout settings must be +coordinated to avoid block rejections. See the WARNING comments in +[`mainnet-miner-conf.toml`](../sample/conf/mainnet-miner-conf.toml) and +[`mainnet-signer-conf.toml`](../sample/conf/signer/mainnet-signer-conf.toml) for details. + +Key interactions: + +- **`tenure_extend_wait_timeout_ms`** (miner) must be >= **`block_proposal_timeout_ms`** (signer). + The signer waits `block_proposal_timeout_ms` before marking an unresponsive miner as inactive. + If the miner extends before the signer invalidates the new winner, the extend is rejected. + +- **`tenure_timeout_secs`** (miner) should be > signer's **`tenure_idle_timeout_secs + tenure_idle_timeout_buffer_secs`** (default 62s). + The signer computes an extend timestamp from the last block time + idle timeout + buffer. + The miner must wait at least this long before time-based extends. + +- **`min_time_between_blocks_ms`** (miner) must be >= 1000ms. + Blocks with same-second timestamps as their parent are rejected network-wide. + +## Running + +```bash +# Start the node +stacks-node start --config mainnet-signer.toml + +# Start the signer +stacks-signer run --config mainnet-signer-conf.toml +``` + +## Further Reading + +- [Comprehensive signer config reference](../sample/conf/signer/mainnet-signer-conf.toml) +- [Comprehensive miner config reference](../sample/conf/mainnet-miner-conf.toml) +- [Mining documentation](mining.md) +- [Follower documentation](follower.md) diff --git a/pox-locking/src/events.rs b/pox-locking/src/events.rs index 031853adef3..f439b1ed826 100644 --- a/pox-locking/src/events.rs +++ b/pox-locking/src/events.rs @@ -652,24 +652,22 @@ fn inner_synthesize_pox_event_info( sender.clone(), None, pox_contract.contract_context, - |env| { - let base_event_info = - env.eval_read_only(contract_id, &code_snippet) - .map_err(|e| { - error!( + |exec_state, invoke_ctx| { + let base_event_info = exec_state + .eval_read_only(invoke_ctx, contract_id, &code_snippet) + .map_err(|e| { + error!( "Failed to run event-info code snippet for '{function_name}': {e:?}" ); - e - })?; + e + })?; - let data_event_info = - env.eval_read_only(contract_id, &data_snippet) - .map_err(|e| { - error!( - "Failed to run data-info code snippet for '{function_name}': {e:?}" - ); - e - })?; + let data_event_info = exec_state + .eval_read_only(invoke_ctx, contract_id, &data_snippet) + .map_err(|e| { + error!("Failed to run data-info code snippet for '{function_name}': {e:?}"); + e + })?; // merge them let base_event_tuple = base_event_info diff --git a/pox-locking/src/events_24.rs b/pox-locking/src/events_24.rs index f9819799f08..10004c30fd1 100644 --- a/pox-locking/src/events_24.rs +++ b/pox-locking/src/events_24.rs @@ -384,24 +384,22 @@ pub fn synthesize_pox_2_or_3_event_info( sender.clone(), None, pox_2_contract.contract_context, - |env| { - let base_event_info = - env.eval_read_only(contract_id, &code_snippet) - .map_err(|e| { - error!( + |exec_state, invoke_ctx| { + let base_event_info = exec_state + .eval_read_only(invoke_ctx, contract_id, &code_snippet) + .map_err(|e| { + error!( "Failed to run event-info code snippet for '{function_name}': {e:?}" ); - e - })?; + e + })?; - let data_event_info = - env.eval_read_only(contract_id, &data_snippet) - .map_err(|e| { - error!( - "Failed to run data-info code snippet for '{function_name}': {e:?}" - ); - e - })?; + let data_event_info = exec_state + .eval_read_only(invoke_ctx, contract_id, &data_snippet) + .map_err(|e| { + error!("Failed to run data-info code snippet for '{function_name}': {e:?}"); + e + })?; // merge them let base_event_tuple = base_event_info diff --git a/pox-locking/src/pox_2.rs b/pox-locking/src/pox_2.rs index d1eb35fe552..e2b605786eb 100644 --- a/pox-locking/src/pox_2.rs +++ b/pox-locking/src/pox_2.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -15,14 +15,14 @@ // along with this program. If not, see . use clarity::boot_util::boot_code_id; -use clarity::vm::contexts::GlobalContext; +use clarity::vm::contexts::{ExecutionState, GlobalContext}; use clarity::vm::costs::cost_functions::ClarityCostFunction; use clarity::vm::costs::runtime_cost; use clarity::vm::database::{ClarityDatabase, STXBalance}; use clarity::vm::errors::{RuntimeError, VmExecutionError}; use clarity::vm::events::{STXEventType, STXLockEventData, StacksTransactionEvent}; use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; -use clarity::vm::{Environment, Value}; +use clarity::vm::Value; use stacks_common::{debug, error}; use crate::events::synthesize_pox_event_info; @@ -514,8 +514,10 @@ pub fn handle_contract_call( if let Some(event_info) = event_info_opt { let event_response = Value::okay(event_info).expect("FATAL: failed to construct (ok event-info)"); - let tx_event = - Environment::construct_print_transaction_event(contract_id, &event_response); + let tx_event = ExecutionState::construct_print_transaction_event( + contract_id.clone(), + event_response, + ); Some(tx_event) } else { None diff --git a/pox-locking/src/pox_3.rs b/pox-locking/src/pox_3.rs index a8aadcd1555..1aefe8ec05d 100644 --- a/pox-locking/src/pox_3.rs +++ b/pox-locking/src/pox_3.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -15,14 +15,14 @@ // along with this program. If not, see . use clarity::boot_util::boot_code_id; -use clarity::vm::contexts::GlobalContext; +use clarity::vm::contexts::{ExecutionState, GlobalContext}; use clarity::vm::costs::cost_functions::ClarityCostFunction; use clarity::vm::costs::runtime_cost; use clarity::vm::database::{ClarityDatabase, STXBalance}; use clarity::vm::errors::{RuntimeError, VmExecutionError}; use clarity::vm::events::{STXEventType, STXLockEventData, StacksTransactionEvent}; use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; -use clarity::vm::{Environment, Value}; +use clarity::vm::Value; use stacks_common::{debug, error}; use crate::events::synthesize_pox_event_info; @@ -404,8 +404,10 @@ pub fn handle_contract_call( if let Some(event_info) = event_info_opt { let event_response = Value::okay(event_info).expect("FATAL: failed to construct (ok event-info)"); - let tx_event = - Environment::construct_print_transaction_event(contract_id, &event_response); + let tx_event = ExecutionState::construct_print_transaction_event( + contract_id.clone(), + event_response, + ); Some(tx_event) } else { None diff --git a/pox-locking/src/pox_4.rs b/pox-locking/src/pox_4.rs index d9f0a9a411d..b429278af96 100644 --- a/pox-locking/src/pox_4.rs +++ b/pox-locking/src/pox_4.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -15,14 +15,14 @@ // along with this program. If not, see . use clarity::boot_util::boot_code_id; -use clarity::vm::contexts::GlobalContext; +use clarity::vm::contexts::{ExecutionState, GlobalContext}; use clarity::vm::costs::cost_functions::ClarityCostFunction; use clarity::vm::costs::runtime_cost; use clarity::vm::database::{ClarityDatabase, STXBalance}; use clarity::vm::errors::{RuntimeError, VmExecutionError, VmInternalError}; use clarity::vm::events::{STXEventType, STXLockEventData, StacksTransactionEvent}; use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; -use clarity::vm::{Environment, Value}; +use clarity::vm::Value; use stacks_common::{debug, error}; use crate::events::synthesize_pox_event_info; @@ -370,8 +370,10 @@ pub fn handle_contract_call( if let Some(event_info) = event_info_opt { let event_response = Value::okay(event_info).expect("FATAL: failed to construct (ok event-info)"); - let tx_event = - Environment::construct_print_transaction_event(contract_id, &event_response); + let tx_event = ExecutionState::construct_print_transaction_event( + contract_id.clone(), + event_response, + ); Some(tx_event) } else { None diff --git a/sample/conf/mainnet-follower-conf.toml b/sample/conf/mainnet-follower-conf.toml index 226fcae806c..11c6fe5f7cc 100644 --- a/sample/conf/mainnet-follower-conf.toml +++ b/sample/conf/mainnet-follower-conf.toml @@ -1,24 +1,61 @@ +# ============================================================ +# STACKS FOLLOWER NODE - MAINNET CONFIGURATION +# ============================================================ +# +# A follower is a read-only node that syncs the Stacks chain without +# mining or signing. Use this to run an API node, monitor the chain, +# or serve RPC requests. +# +# For mining, see mainnet-miner-conf.toml. +# For signing, see signer/mainnet-signer-conf.toml + mainnet-signer.toml. + [node] -# working_dir = "/dir/to/save/chainstate" # defaults to: /tmp/stacks-node-[0-9]* +# IMPORTANT: For production, set this to a persistent path. +# The default (/tmp/stacks-node-) is lost on reboot. +# working_dir = "/stacks-data/mainnet" + rpc_bind = "0.0.0.0:20443" p2p_bind = "0.0.0.0:20444" -prometheus_bind = "0.0.0.0:9153" +prometheus_bind = "0.0.0.0:9153" + +# Explicitly not a miner or signer. +miner = false +stacker = false + +# Mainnet bootstrap nodes (4 seeds) are auto-populated when mode = "mainnet". +# Only override if you need custom seeds (replaces all 4 defaults): +# bootstrap_node = "02196f005965cebe6ddc3901b7b1cc1aa7a88f305bb8c5893456b8f9a605923893@seed.mainnet.hiro.so:20444" + +# Enable transaction indexing for API queries. +# Required if running stacks-blockchain-api. +# Default: false +# txindex = true [burnchain] mode = "mainnet" peer_host = "127.0.0.1" +# rpc_port = 8332 # Bitcoin mainnet RPC (default) +# peer_port = 8333 # Bitcoin mainnet P2P (default) + +# Bitcoin RPC credentials (required by most bitcoind instances). +# username = "your-bitcoin-rpc-user" +# password = "your-bitcoin-rpc-password" -# Used for sending events to a local stacks-blockchain-api service +# Optional: stacks-blockchain-api event observer. # [[events_observer]] # endpoint = "localhost:3700" # events_keys = ["*"] # timeout_ms = 60_000 -# Used if running a local stacks-signer service +# To upgrade this node to also serve a signer, you need: +# 1. Set stacker = true in [node] +# 2. Uncomment the signer events_observer below +# 3. Uncomment the [connection_options] auth_token below +# 4. See mainnet-signer.toml for the full signer node config +# # [[events_observer]] # endpoint = "127.0.0.1:30000" # events_keys = ["stackerdb", "block_proposal", "burn_blocks"] - -# Used if running a local stacks-signer service +# # [connection_options] -# auth_token = "" # fill with a unique password +# auth_token = "your-secret-token" diff --git a/sample/conf/mainnet-miner-conf.toml b/sample/conf/mainnet-miner-conf.toml index 2b6b0ace5a0..df8079b520f 100644 --- a/sample/conf/mainnet-miner-conf.toml +++ b/sample/conf/mainnet-miner-conf.toml @@ -1,22 +1,439 @@ +# ============================================================ +# STACKS MINER - MAINNET REFERENCE CONFIGURATION +# ============================================================ +# +# This is a comprehensive reference configuration for running a +# Stacks miner on mainnet. All settings are documented with their +# default values. Required settings are marked as REQUIRED. +# +# To use: copy this file, uncomment and customize the settings you +# need, and remove the rest. At minimum, you must set: +# - [node] seed +# - [burnchain] username + password (Bitcoin RPC credentials) +# - [connection_options] auth_token (must match signer's auth_password) +# - [[events_observer]] endpoint (must match signer's endpoint) +# +# Lines with "# key = value" show the default; uncomment to override. +# Lines with "key = ..." are REQUIRED or recommended to set explicitly. + +# ============================================================ +# [node] - Core Node Settings +# ============================================================ [node] -# working_dir = "/dir/to/save/chainstate" # defaults to: /tmp/stacks-node-[0-9]* -rpc_bind = "127.0.0.1:20443" -p2p_bind = "127.0.0.1:20444" -prometheus_bind = "127.0.0.1:9153" -seed = "" + +# REQUIRED: The seed (private key) for the miner's Bitcoin wallet. +# This is the hex-encoded private key used for burnchain operations. +seed = "" + +# Enable mining. Must be true for a miner node. miner = true -mine_microblocks = false # Disable microblocks (ref: https://github.com/stacks-network/stacks-core/pull/4561 ) +# Enable stacker support (StackerDB replication). +# Should be true when running with a signer. +# Default: false +stacker = true + +# HTTP RPC server bind address. +# Default: "0.0.0.0:20443" +rpc_bind = "0.0.0.0:20443" + +# P2P networking bind address. +# Default: "0.0.0.0:20444" +p2p_bind = "0.0.0.0:20444" + +# Data directory for chainstate, burnchain databases, etc. +# Default: /tmp/stacks-node- +# Can also be set via the STACKS_WORKING_DIR environment variable. +# working_dir = "/var/lib/stacks-node" + +# Prometheus metrics endpoint. Uncomment to enable metrics. +# prometheus_bind = "0.0.0.0:9153" + +# Bootstrap peer(s) for initial sync. Format: "PUBKEY@HOST:PORT" +# For mainnet, the node will use built-in seed nodes if omitted. +# bootstrap_node = "02e...@seed.mainnet.hiro.so:20444" + +# Human-readable node name (used in logging). +# Default: "helium-node" +# name = "my-stacks-miner" + +# Disable microblocks (not used in Nakamoto / Epoch 3.0+). +mine_microblocks = false + +# Enable transaction indexing for API queries. +# Default: false +# txindex = true + +# Chain liveness watchdog poll interval. +# Default: 300 +# Units: seconds +# chain_liveness_poll_time_secs = 300 + + +# ============================================================ +# [burnchain] - Bitcoin Connection & Mining Fee Settings +# ============================================================ [burnchain] + +# Network mode. Must be "mainnet" for mainnet operation. mode = "mainnet" + +# Bitcoin node RPC hostname. +# Default: "0.0.0.0" peer_host = "127.0.0.1" -username = "" -password = "" -# Maximum amount (in sats) of "burn commitment" to broadcast for the next block's leader election + +# Bitcoin node RPC port. +# Default: 8332 +# rpc_port = 8332 + +# Bitcoin node P2P port. +# Default: 8333 +# peer_port = 8333 + +# REQUIRED: Bitcoin RPC credentials. +username = "" +password = "" + +# Use SSL for Bitcoin RPC connection. +# Default: false +# rpc_ssl = false + +# Maximum amount (in sats) of "burn commitment" to broadcast for +# the next block's leader election. +# Default: 20_000 +# Units: satoshis burn_fee_cap = 20000 -# Amount (in sats) per byte - Used to calculate the transaction fees + +# Fee rate for Bitcoin transactions. +# Default: 50 +# Units: satoshis per virtual byte (sats/vB) satoshis_per_byte = 25 -# Amount of sats to add when RBF'ing bitcoin tx (default: 5) + +# Amount of sats/vB to add when RBF'ing a Bitcoin transaction. +# Default: 5 +# Units: satoshis per virtual byte rbf_fee_increment = 5 -# Maximum percentage to RBF bitcoin tx (default: 150% of satsv/B) + +# Maximum percentage to increase fee rate when RBF'ing. +# For example, 150 means the fee can increase up to 150% of the +# original sats/vB rate. +# Default: 150 +# Units: percentage max_rbf = 150 + +# Bitcoin RPC request timeout. +# Default: 300 +# Units: seconds +# timeout = 300 + +# Bitcoin socket operation timeout. +# Default: 30 +# Units: seconds +# socket_timeout = 30 + +# Bitcoin poll interval. How often to check for new Bitcoin blocks. +# Default: 10 +# Units: seconds +# poll_time_secs = 10 + +# Bitcoin wallet name (if using multi-wallet Bitcoin Core). +# Default: "" (default wallet) +# wallet_name = "" + +# Wait time before attempting to build a block after a burnchain event. +# Default: 5_000 +# Units: milliseconds +# commit_anchor_block_within = 5000 + + +# ============================================================ +# [miner] - Nakamoto Mining Settings +# ============================================================ +# +# This section controls block production, signer coordination, +# tenure management, and mempool behavior for Nakamoto mining. +# +[miner] + +# The private key used for signing Stacks blocks. +# If omitted, derived from the [node] seed. +# Format: hex-encoded secp256k1 private key +# mining_key = "" + +# Whether to use a segwit (p2wpkh) Bitcoin address for mining. +# Default: false +# segwit = false + +# Path to a file containing an already-activated VRF key. +# If set, the miner will skip the VRF key registration step. +# activated_vrf_key_path = "/path/to/vrf_key.json" + +# Override the coinbase reward recipient address. +# If set, block rewards are sent to this address instead of the miner's. +# Format: a standard Stacks principal (e.g., "SP1EXAMPLE...") +# Default: None (rewards go to the miner) +# block_reward_recipient = "SP1EXAMPLE..." + +# --- Block Timing --- + +# Time limit for assembling a Nakamoto block (transaction selection). +# Default: 5_000 +# Units: milliseconds +# nakamoto_attempt_time_ms = 5000 + +# Minimum time between consecutive Stacks blocks. The miner will not +# propose a block until this much time has passed since the parent. +# +# WARNING: Must be >= 1000. Blocks with same-second timestamps as their +# parent are rejected network-wide. The miner enforces this locally, but +# even if bypassed, signers would also reject such blocks. +# +# Default: 1_000 +# Units: milliseconds +# min_time_between_blocks_ms = 1000 + +# Sleep time when the mempool is empty (before checking again). +# Default: 2_500 +# Units: milliseconds +# empty_mempool_sleep_ms = 2500 + +# Delay before broadcasting a block-commit transaction after winning +# a sortition, to allow late-arriving Bitcoin blocks to be processed. +# Default: 40_000 +# Units: milliseconds +# block_commit_delay_ms = 40000 + +# --- Signer Coordination --- + +# How long to wait for the new sortition winner to produce a block before +# the miner attempts to extend its own tenure. +# +# When a new sortition happens and a different miner wins, the current miner +# gives the winner this much time to produce a block. If the winner is +# unresponsive, the current miner sends a TenureChange::Extended block. +# +# WARNING: Interacts with signer's `block_proposal_timeout_ms` (default 120_000ms). +# The signer independently waits `block_proposal_timeout_ms` before marking +# the new sortition winner as inactive. The signer will reject tenure extends +# from the previous miner until it has timed out the new winner. +# +# If this value < signer's `block_proposal_timeout_ms`: +# Miner extends BEFORE signer times out the new winner -> REJECTED +# If this value >= signer's `block_proposal_timeout_ms`: +# Signer times out new winner first, then accepts the extend -> OK +# +# Additionally, the signer requires `tenure_idle_timeout_secs + tenure_idle_timeout_buffer_secs` +# (default 62s) to have passed since the last block before accepting any extend. +# Both conditions must be met on the signer side. +# +# Default: 120_000 +# Units: milliseconds +# tenure_extend_wait_timeout_ms = 120000 + +# Maximum time a miner holds a tenure before issuing a time-based +# tenure extend (even if the miner itself won the sortition). +# +# This is checked alongside the signer-provided `tenure_extend_timestamp` +# (which is computed from `tenure_idle_timeout_secs + tenure_idle_timeout_buffer_secs`). The miner +# will only extend when BOTH this timeout has elapsed AND the signer's +# timestamp allows it. +# +# WARNING: Should be greater than `tenure_extend_wait_timeout_ms` and +# greater than signer's `tenure_idle_timeout_secs + tenure_idle_timeout_buffer_secs` (default 62s). +# +# Default: 180 +# Units: seconds +# tenure_timeout_secs = 180 + +# Pause duration after the first block rejection from signers. +# Default: 5_000 +# Units: milliseconds +# first_rejection_pause_ms = 5000 + +# Pause duration after subsequent (2nd, 3rd, ...) block rejections. +# Default: 10_000 +# Units: milliseconds +# subsequent_rejection_pause_ms = 10000 + +# Adaptive timeout steps based on signer rejection weight. Keys are +# percentages of total signer weight that has rejected; values are +# timeout durations (in seconds) to wait before abandoning the proposal. +# +# The miner tracks accumulated rejection weight as signers reject. +# It looks up the highest percentage key <= current rejection weight % +# and uses that timeout. When the timeout expires without reaching +# approval threshold, the miner abandons the proposal and retries. +# +# WARNING: Directly affects when the miner gives up on a block proposal. +# Lower timeouts at higher rejection percentages cause faster retries. +# At "30" = 0, the miner gives up immediately when 30% of signer weight +# has rejected. +# +# Default: +# "0" = 180 (0% rejected: wait up to 180s for signatures) +# "10" = 90 (10%+ signer weight rejected: reduce wait to 90s) +# "20" = 45 (20%+ signer weight rejected: reduce wait to 45s) +# "30" = 0 (30%+ signer weight rejected: give up immediately) +# +# Units: keys = percentage (0-100), values = seconds +# [miner.block_rejection_timeout_steps] +# "0" = 180 +# "10" = 90 +# "20" = 45 +# "30" = 0 + +# --- Cost & Budget Limits --- + +# Percentage of the total tenure execution budget that a single block +# can consume. Prevents one block from using the entire tenure budget. +# Valid range: 1 - 100 +# Default: 25 +# Units: percentage of tenure budget +# tenure_cost_limit_per_block_percentage = 25 + +# When a block's execution cost reaches this percentage of the budget, +# non-boot contract calls are excluded from further inclusion. +# Reserves remaining capacity for critical system operations. +# Valid range: 0 - 100 +# Default: 95 +# Units: percentage of block budget +# contract_cost_limit_percentage = 95 + +# Maximum execution time allowed for a single transaction. +# NOTE: When the [miner] section is present and this field is omitted, +# the effective value is None (no timeout). The default of 30 only +# applies when the entire [miner] section is absent. +# Default: 30 (when [miner] section is absent) +# Units: seconds +# max_execution_time_secs = 30 + +# Tenure execution budget percentage that triggers a cost-based +# tenure extend. +# Default: 50 +# Units: percentage (0-100) +# tenure_extend_cost_threshold = 50 + +# Maximum total size of all blocks in a tenure. +# Default: 10_485_760 (10 MB) +# Units: bytes +# max_tenure_bytes = 10485760 + +# --- Mempool --- + +# Strategy for selecting transactions from the mempool during block building. +# Valid values: +# "GlobalFeeRate" - Select transactions with the highest global fee rate +# "NextNonceWithHighestFeeRate" - Select transactions with the next expected +# nonce for each origin/sponsor, preferring highest fee rate +# Default: "NextNonceWithHighestFeeRate" +# mempool_walk_strategy = "NextNonceWithHighestFeeRate" + +# Comma-separated list of transaction types to consider for inclusion. +# Valid types: "TokenTransfer", "SmartContract", "ContractCall" +# Default: all types +# txs_to_consider = "TokenTransfer,SmartContract,ContractCall" + +# Comma-separated list of Stacks addresses to whitelist. +# Only transactions from these origins will be included in blocks. +# Default: empty (all origins accepted) +# filter_origins = "" + +# Probability (0-100) of selecting a transaction with no fee estimate. +# Default: 25 +# probability_pick_no_estimate_tx = 25 + +# Size of the nonce cache used during mempool walks. +# Default: 1_048_576 +# Units: items (LRU cache entries) +# nonce_cache_size = 1048576 + +# Size of the candidate retry cache for the GlobalFeeRate mempool walk strategy. +# Default: 1_048_576 +# Units: items +# candidate_retry_cache_size = 1048576 + +# --- Tenure Extension Polling --- + +# How often the miner checks whether a tenure extend is needed. +# Default: 1 +# Units: seconds +# tenure_extend_poll_secs = 1 + +# --- Advanced / Debugging --- + +# Replay expected transactions during block building (experimental). +# WARNING: Cannot be set to true on mainnet (node will fail to start). +# Default: false +# replay_transactions = false + +# StackerDB socket timeout for miner operations. +# Default: 120 +# Units: seconds +# stackerdb_timeout_secs = 120 + +# Log transactions that are skipped during mempool walk. +# Default: false +# log_skipped_transactions = false + + +# ============================================================ +# [connection_options] - Network & Authentication +# ============================================================ +[connection_options] + +# REQUIRED: Authentication token for the block proposal HTTP endpoint. +# +# WARNING: This must match the `auth_password` in your signer's config. +# If these do not match, the signer cannot communicate with the node +# and block proposals will fail silently. +auth_token = "" + +# Maximum age of block proposals accepted by this node. +# Default: 600 +# Units: seconds +# block_proposal_max_age_secs = 600 + +# Timeout for validating block proposals from miners. +# Default: 60 +# Units: seconds +# block_proposal_validation_timeout_secs = 60 + +# Maximum number of concurrent HTTP connections. +# Default: 1_000 +# max_http_clients = 1000 + +# Target number of P2P neighbor connections. +# Default: 32 +# num_neighbors = 32 + +# Maximum number of inbound P2P connections. +# Default: 750 +# num_clients = 750 + +# Allow connections from private IP ranges (e.g., 10.x, 192.168.x). +# Default: false +# private_neighbors = false + +# Override the publicly advertised IP:port for this node. +# Default: auto-detected +# public_ip_address = "1.2.3.4:20444" + + +# ============================================================ +# [[events_observer]] - Event Subscriptions +# ============================================================ + +# Signer event observer (REQUIRED for signer integration). +# +# WARNING: The `endpoint` must match your signer's `endpoint` config. +# The `events_keys` must include "stackerdb", "block_proposal", and +# "burn_blocks" for proper signer operation. +[[events_observer]] +endpoint = "127.0.0.1:30000" +events_keys = ["stackerdb", "block_proposal", "burn_blocks"] + +# Optional: API event observer for stacks-blockchain-api service. +# [[events_observer]] +# endpoint = "localhost:3700" +# events_keys = ["*"] +# timeout_ms = 60_000 diff --git a/sample/conf/mainnet-mockminer-conf.toml b/sample/conf/mainnet-mockminer-conf.toml index 9487034e547..89ce0f80b62 100644 --- a/sample/conf/mainnet-mockminer-conf.toml +++ b/sample/conf/mainnet-mockminer-conf.toml @@ -1,17 +1,39 @@ +# ============================================================ +# STACKS MOCK MINER - MAINNET CONFIGURATION +# ============================================================ +# +# A mock miner follows the chain and assembles blocks locally without +# spending BTC or proposing to signers. Useful for testing block +# assembly logic, verifying transaction processing, and monitoring +# what a miner would produce. +# +# See mainnet-miner-conf.toml for a real miner configuration. + [node] -# working_dir = "/dir/to/save/chainstate" # defaults to: /tmp/stacks-node-[0-9]* +# IMPORTANT: For production, set this to a persistent path. +# The default (/tmp/stacks-node-) is lost on reboot. +# working_dir = "/stacks-data/mock-miner" + rpc_bind = "0.0.0.0:20443" p2p_bind = "0.0.0.0:20444" prometheus_bind = "0.0.0.0:9153" -# Both miner and mock_mining must be true +# Both miner and mock_mining must be true. miner = true mock_mining = true [miner] -# Required: Generate with: openssl rand -hex 32 +# Required when [miner] section is present. +# Generate with: openssl rand -hex 32 mining_key = "0000000000000000000000000000000000000000000000000000000000000001" [burnchain] mode = "mainnet" peer_host = "127.0.0.1" +# rpc_port = 8332 # Bitcoin mainnet RPC (default) +# peer_port = 8333 # Bitcoin mainnet P2P (default) + +# Bitcoin RPC credentials. The mock miner still needs to follow the +# burnchain for sortition data, even though it never sends transactions. +# username = "your-bitcoin-rpc-user" +# password = "your-bitcoin-rpc-password" diff --git a/sample/conf/mainnet-signer.toml b/sample/conf/mainnet-signer.toml index 8683f076f24..2a52cc33626 100644 --- a/sample/conf/mainnet-signer.toml +++ b/sample/conf/mainnet-signer.toml @@ -1,22 +1,48 @@ +# ============================================================ +# STACKS SIGNER NODE - MAINNET CONFIGURATION +# ============================================================ +# +# This configures the stacks-node to work with a signer on mainnet. +# This is the NODE-SIDE config. For the signer binary config, see +# signer/mainnet-signer-conf.toml. +# +# Key coordination points between this config and the signer binary: +# - [[events_observer]] endpoint must match signer's `endpoint` +# - [connection_options] auth_token must match signer's `auth_password` + [node] # working_dir = "/dir/to/save/chainstate" # defaults to: /tmp/stacks-node-[0-9]* rpc_bind = "0.0.0.0:20443" p2p_bind = "0.0.0.0:20444" -prometheus_bind = "0.0.0.0:9153" +prometheus_bind = "0.0.0.0:9153" +stacker = true [burnchain] mode = "mainnet" peer_host = "127.0.0.1" -# Used for sending events to a local stacks-blockchain-api service +# Signer event observer (REQUIRED). +# WARNING: endpoint must match your signer binary's `endpoint` config. +[[events_observer]] +endpoint = "127.0.0.1:30000" +events_keys = ["stackerdb", "block_proposal", "burn_blocks"] + +# Optional: API event observer for stacks-blockchain-api service # [[events_observer]] # endpoint = "localhost:3700" # events_keys = ["*"] # timeout_ms = 60_000 -[[events_observer]] -endpoint = "127.0.0.1:30000" -events_keys = ["stackerdb", "block_proposal", "burn_blocks"] - [connection_options] -auth_token = "" # fill with a unique password +# WARNING: Must match the signer binary's `auth_password`. +auth_token = "" + +# Maximum age of block proposals accepted by this node. +# Default: 600 +# Units: seconds +# block_proposal_max_age_secs = 600 + +# Timeout for block proposal validation. +# Default: 60 +# Units: seconds +# block_proposal_validation_timeout_secs = 60 diff --git a/sample/conf/mocknet.toml b/sample/conf/mocknet.toml index 607dd405050..fee25e096db 100644 --- a/sample/conf/mocknet.toml +++ b/sample/conf/mocknet.toml @@ -1,14 +1,50 @@ +# ============================================================ +# STACKS MOCKNET - LOCAL DEVELOPMENT CONFIGURATION +# ============================================================ +# +# Mocknet runs a simulated burnchain entirely in-process. No Bitcoin +# node is required. Execution cost limits are removed. Use this for +# local development and testing. +# +# Start with: stacks-node start --config=mocknet.toml + [node] -# working_dir = "/dir/to/save/chainstate" # defaults to: /tmp/stacks-node-[0-9]* +# working_dir = "/stacks-data/mocknet" rpc_bind = "0.0.0.0:20443" p2p_bind = "0.0.0.0:20444" -prometheus_bind = "0.0.0.0:9153" +prometheus_bind = "0.0.0.0:9153" + +# Enable mining so the node produces blocks. +miner = true +mock_mining = true +seed = "0000000000000000000000000000000000000000000000000000000000000000" [burnchain] mode = "mocknet" -# Used for sending events to a local stacks-blockchain-api service +# How long to wait after a burnchain tip before building a block. +# Default: 5_000 +# Units: milliseconds +# commit_anchor_block_within = 5000 + +# Pre-funded test accounts (10M STX each). +[[ustx_balance]] +address = "ST2QKZ4FKHAH1NQKYKYAYZPY440FEPK7GZ1R5HBP2" +amount = 10000000000000000 + +[[ustx_balance]] +address = "ST319CF5WV77KYR1H3GT0GZ7B8Q4AQPY42ETP1VPF" +amount = 10000000000000000 + +[[ustx_balance]] +address = "ST221Z6TDTC5E0BYR2V624Q2ST6R0Q71T78WTAX6H" +amount = 10000000000000000 + +[[ustx_balance]] +address = "ST2TFVBMRPS5SSNP98DQKQ5JNB2B6NZM91C4K3P7B" +amount = 10000000000000000 + +# Optional: stacks-blockchain-api event observer. # [[events_observer]] # endpoint = "localhost:3700" # events_keys = ["*"] - diff --git a/sample/conf/signer/mainnet-signer-conf.toml b/sample/conf/signer/mainnet-signer-conf.toml new file mode 100644 index 00000000000..12fcb9319ac --- /dev/null +++ b/sample/conf/signer/mainnet-signer-conf.toml @@ -0,0 +1,242 @@ +# ============================================================ +# STACKS SIGNER - MAINNET REFERENCE CONFIGURATION +# ============================================================ +# +# This is a comprehensive reference configuration for the stacks-signer +# binary on mainnet. This file configures the SIGNER PROCESS, not the +# stacks-node (see mainnet-signer.toml for the node-side config). +# +# To use: copy this file, fill in the REQUIRED fields, and optionally +# uncomment any settings you want to override. +# +# At minimum, you must set: +# - stacks_private_key (your signer's private key) +# - node_host (your stacks-node's RPC address) +# - endpoint (where this signer listens for events) +# - network ("mainnet") +# - auth_password (must match node's [connection_options] auth_token) +# - db_path (signer database location) +# +# Lines with "# key = value" show the default; uncomment to override. + +# ============================================================ +# Required Settings +# ============================================================ + +# REQUIRED: Hex-encoded Stacks private key for this signer. +# 64 hex chars (uncompressed) or 66 hex chars (with "01" compression suffix). +# This key determines the signer's on-chain identity and STX address. +# Must correspond to the public key used to register this signer via +# stack-stx or stack-aggregation-commit Clarity function calls. +stacks_private_key = "" + +# REQUIRED: The Stacks node RPC endpoint to connect to. +# Must match the node's [node] rpc_bind address. +node_host = "127.0.0.1:20443" + +# REQUIRED: Local endpoint this signer listens on for events from the node. +# Must match the endpoint in the node's [[events_observer]] section. +endpoint = "0.0.0.0:30000" + +# REQUIRED: Network selection. +# Valid values: "mainnet", "testnet", "mocknet" +network = "mainnet" + +# REQUIRED: Authorization password for the node's block proposal endpoint. +# +# WARNING: This MUST match the `auth_token` in the stacks-node's +# [connection_options] section. If they do not match, the signer +# cannot communicate with the node and will fail silently. +auth_password = "" + +# REQUIRED: Path to the signer's SQLite database file. +# Use an absolute path for production deployments. +db_path = "/var/lib/stacks-signer/signerdb.sqlite" + + +# ============================================================ +# Block Proposal Settings +# ============================================================ + +# How long to wait for the current sortition winner to propose a block +# before the signer marks that miner as inactive. +# +# When a new sortition happens, the signer gives the winning miner this +# much time to propose a block. If no proposal arrives, the signer marks +# the winner as InvalidatedBeforeFirstBlock. This is one of two gates +# that must be satisfied before the signer will accept a tenure extend +# from the PREVIOUS miner (the other gate is `tenure_idle_timeout_secs`). +# +# WARNING: Interacts with miner's `tenure_extend_wait_timeout_ms` (default 120_000ms). +# The miner waits `tenure_extend_wait_timeout_ms` before attempting to extend. +# +# If miner's value < this value: +# Miner extends BEFORE signer invalidates the new winner -> REJECTED +# If miner's value >= this value: +# Signer invalidates new winner first, then accepts extend -> OK +# +# Recommended: keep this <= miner's tenure_extend_wait_timeout_ms. +# +# Default: 120_000 +# Units: milliseconds +# block_proposal_timeout_ms = 120000 + +# How long to wait for the node to validate a block proposal before +# marking the block as invalid and rejecting it. +# Default: 120_000 +# Units: milliseconds +# block_proposal_validation_timeout_ms = 120000 + +# Maximum age of a block proposal that will be processed. +# Proposals older than this are silently dropped. +# Default: 600 +# Units: seconds +# block_proposal_max_age_secs = 600 + + +# ============================================================ +# Tenure Management Settings +# ============================================================ + +# How much time since the last block in a tenure must pass before the +# signer will allow a tenure extend. +# +# When the signer accepts a block, it computes an extend timestamp: +# extend_timestamp = last_block_time + tenure_idle_timeout_secs + tenure_idle_timeout_buffer_secs +# The signer includes this timestamp in its BlockAccepted response. +# The miner cannot extend until current_time >= extend_timestamp. +# +# This is one of two gates for tenure extends (the other is +# `block_proposal_timeout_ms` for new-winner invalidation). +# +# WARNING: Must coordinate with the miner's settings: +# - Miner `tenure_timeout_secs` (default 180s): must be > this + buffer +# - Miner `tenure_extend_wait_timeout_ms` (default 120_000ms): should +# be >= this + buffer so the miner doesn't extend too early +# +# Default: 60 +# Units: seconds +# tenure_idle_timeout_secs = 60 + +# Buffer added to the tenure idle timeout to account for clock skew +# between signer and miner nodes. The effective idle timeout sent to +# miners is: tenure_idle_timeout_secs + tenure_idle_timeout_buffer_secs. +# +# Default: 2 +# Units: seconds +# tenure_idle_timeout_buffer_secs = 2 + +# How much idle time must pass before allowing a read-count tenure extend. +# Triggered when the read-count budget is nearly exhausted. +# Default: 20 +# Units: seconds +# read_count_idle_timeout_secs = 20 + +# Time to wait for the last block of a tenure to be globally accepted +# or rejected before considering a new miner's block at the same height +# as potentially valid. +# Default: 30 +# Units: seconds +# tenure_last_block_proposal_timeout_secs = 30 + + +# ============================================================ +# Miner Coordination Settings +# ============================================================ + +# Reorg protection window. Measures the time between when the first block +# of a tenure was signed and when the next burn block (sortition) arrived. +# +# If a new miner tries to reorg a tenure that already produced blocks: +# - If (burn_block_received - first_block_signed) < this value: +# Reorg is ALLOWED (the tenure was "poorly timed" and the incoming +# miner did not have sufficient time to RBF an outdated commit) +# - If (burn_block_received - first_block_signed) >= this value: +# Reorg is DENIED (the tenure was established long enough for the +# incoming miner to RBF any outdated commit) +# +# WARNING: Setting this too LOW allows dangerous reorgs of established +# tenures. Setting it too HIGH blocks legitimate miner handoffs when +# the previous tenure's first block arrived shortly before the sortition. +# +# Default: 60 +# Units: seconds +# first_proposal_burn_block_timing_secs = 60 + +# Time following a block's global acceptance during which the signer will +# consider a miner's reorg attempt as valid miner activity (not malicious). +# Default: 200_000 +# Units: milliseconds +# reorg_attempts_activity_timeout_ms = 200000 + + +# ============================================================ +# State Machine Settings +# ============================================================ + +# Time to wait between updating the local state machine view and +# capitulating to the consensus view of other signers. +# Lower values mean faster convergence; higher values give more time +# for independent verification. +# +# WARNING: Setting this too low can cause the signer to flip-flop +# between viewpoints. It must allow enough time for other signers to +# receive updates and publish their own updated views before this +# signer gives up on its own viewpoint and converges. +# +# Default: 20 +# Units: seconds +# capitulate_miner_view_timeout_secs = 20 + +# Time to wait before submitting a block proposal if the signer cannot +# confirm the stacks-node has processed the parent block. +# Default: 15 +# Units: seconds +# proposal_wait_for_parent_time_secs = 15 + + +# ============================================================ +# Advanced Settings +# ============================================================ + +# Run in dry-run mode. The signer logs actions but does not submit +# StackerDB messages or participate in signing. Useful for testing +# or monitoring. +# +# WARNING: If you enable dry_run, make sure this signer is NOT running +# with a valid registered signer key. A registered signer in dry-run +# mode will not participate in signing, which harms network liveness. +# +# Default: false +# dry_run = false + +# Enforce transaction replay during stacks block validation following a +# bitcoin block reorg (experimental). Ensures that a miner includes the +# expected transactions from reorged stacks blocks that can be replayed. +# Default: false +# validate_with_replay_tx = false + +# Number of bitcoin blocks after a bitcoin fork to reset the replay set. +# Acts as a failsafe to ensure that signers do not permanently prevent +# valid stacks block production based solely on transaction replay. +# Default: 2 +# Units: bitcoin blocks +# reset_replay_set_after_fork_blocks = 2 + +# HTTP timeout for StackerDB read/write operations. +# Default: 120 +# Units: seconds +# stackerdb_timeout_secs = 120 + +# Timeout for receiving events from the stacks-node. +# Default: 5_000 +# Units: milliseconds +# event_timeout_ms = 5000 + +# Custom Chain ID (only for private/custom networks). +# Default: 0x00000001 (mainnet) or 0x80000000 (testnet) +# chain_id = 1 + +# Prometheus metrics endpoint. Uncomment to enable. +# Format: "host:port" +# metrics_endpoint = "0.0.0.0:9090" diff --git a/sample/conf/testnet-follower-conf.toml b/sample/conf/testnet-follower-conf.toml index bce54f4295d..abf284e5f53 100644 --- a/sample/conf/testnet-follower-conf.toml +++ b/sample/conf/testnet-follower-conf.toml @@ -1,31 +1,53 @@ +# ============================================================ +# STACKS FOLLOWER NODE - TESTNET CONFIGURATION +# ============================================================ +# +# A follower is a read-only node that syncs the Stacks chain without +# mining or signing. This config uses the Hiro-hosted testnet (krypton). +# +# For testnet mining, see testnet-miner-conf.toml. +# For testnet signing, see testnet-signer.toml. + [node] -# working_dir = "/dir/to/save/chainstate" # defaults to: /tmp/stacks-node-[0-9]* +# IMPORTANT: For persistent state, set this to a stable path. +# The default (/tmp/stacks-node-) is lost on reboot. +# working_dir = "/stacks-data/testnet" + rpc_bind = "0.0.0.0:20443" p2p_bind = "0.0.0.0:20444" bootstrap_node = "029266faff4c8e0ca4f934f34996a96af481df94a89b0c9bd515f3536a95682ddc@seed.testnet.hiro.so:30444" -prometheus_bind = "0.0.0.0:9153" +prometheus_bind = "0.0.0.0:9153" + +# Explicitly not a miner or signer. +miner = false +stacker = false [burnchain] mode = "krypton" peer_host = "bitcoin.regtest.hiro.so" +rpc_port = 18443 peer_port = 18444 pox_prepare_length = 100 pox_reward_length = 900 -# Used for sending events to a local stacks-blockchain-api service +# Optional: stacks-blockchain-api event observer. # [[events_observer]] # endpoint = "localhost:3700" # events_keys = ["*"] # timeout_ms = 60_000 -# Used if running a local stacks-signer service +# To upgrade this node to also serve a signer, you need: +# 1. Set stacker = true in [node] +# 2. Uncomment the signer events_observer below +# 3. Uncomment the [connection_options] auth_token below +# 4. See testnet-signer.toml for the full signer node config +# # [[events_observer]] # endpoint = "127.0.0.1:30000" # events_keys = ["stackerdb", "block_proposal", "burn_blocks"] - -# Used if running a local stacks-signer service +# # [connection_options] -# auth_token = "" # fill with a unique password +# auth_token = "your-secret-token" [[ustx_balance]] address = "ST2QKZ4FKHAH1NQKYKYAYZPY440FEPK7GZ1R5HBP2" @@ -90,3 +112,7 @@ start_height = 71525 [[burnchain.epochs]] epoch_name = "3.3" start_height = 109280 + +[[burnchain.epochs]] +epoch_name = "3.4" +start_height = 159645 diff --git a/sample/conf/testnet-miner-conf.toml b/sample/conf/testnet-miner-conf.toml index d094627ffbe..e73952f2efb 100644 --- a/sample/conf/testnet-miner-conf.toml +++ b/sample/conf/testnet-miner-conf.toml @@ -1,17 +1,28 @@ +# ============================================================ +# STACKS MINER - TESTNET REFERENCE CONFIGURATION +# ============================================================ +# +# Testnet miner configuration. See mainnet-miner-conf.toml for +# comprehensive documentation of all settings. + [node] # working_dir = "/dir/to/save/chainstate" # defaults to: /tmp/stacks-node-[0-9]* rpc_bind = "0.0.0.0:20443" p2p_bind = "0.0.0.0:20444" bootstrap_node = "029266faff4c8e0ca4f934f34996a96af481df94a89b0c9bd515f3536a95682ddc@seed.testnet.hiro.so:30444" -prometheus_bind = "0.0.0.0:9153" +prometheus_bind = "0.0.0.0:9153" +seed = "" +miner = true +mine_microblocks = false +stacker = true [burnchain] mode = "krypton" peer_host = "127.0.0.1" username = "" password = "" -rpc_port = 12345 # Bitcoin RPC port -peer_port = 6789 # Bitcoin P2P port +rpc_port = 18443 # Bitcoin regtest RPC port +peer_port = 18444 # Bitcoin regtest P2P port pox_prepare_length = 100 pox_reward_length = 900 # Maximum amount (in sats) of "burn commitment" to broadcast for the next block's leader election @@ -23,6 +34,56 @@ rbf_fee_increment = 5 # Maximum percentage to RBF bitcoin tx (default: 150% of satsv/B) max_rbf = 150 +# ============================================================ +# [miner] - Nakamoto Mining Settings +# ============================================================ +# See mainnet-miner-conf.toml for full documentation of all options. +[miner] +# mining_key = "" + +# Time limit for assembling a Nakamoto block. +# Default: 5_000 ms +# nakamoto_attempt_time_ms = 5000 + +# WARNING: Must be >= 1000. Blocks with same-second timestamps are rejected. +# Default: 1_000 ms +# min_time_between_blocks_ms = 1000 + +# WARNING: Must be >= signer's block_proposal_timeout_ms (default 120_000ms). +# If lower, miner extends before signer invalidates new winner -> rejections. +# Default: 120_000 ms +# tenure_extend_wait_timeout_ms = 120000 + +# WARNING: Should be > tenure_extend_wait_timeout_ms and > signer's +# tenure_idle_timeout_secs + tenure_idle_timeout_buffer_secs (default 62s). +# Default: 180 seconds +# tenure_timeout_secs = 180 + +# Default: 40_000 ms +# block_commit_delay_ms = 40000 + +# ============================================================ +# [connection_options] - Authentication for signer communication +# ============================================================ +[connection_options] +# WARNING: Must match the signer's auth_password. +auth_token = "" + +# ============================================================ +# [[events_observer]] - Signer event subscription +# ============================================================ + +# WARNING: endpoint must match your signer's endpoint config. +[[events_observer]] +endpoint = "127.0.0.1:30000" +events_keys = ["stackerdb", "block_proposal", "burn_blocks"] + +# Optional: API event observer +# [[events_observer]] +# endpoint = "localhost:3700" +# events_keys = ["*"] +# timeout_ms = 60_000 + [[ustx_balance]] address = "ST2QKZ4FKHAH1NQKYKYAYZPY440FEPK7GZ1R5HBP2" amount = 10000000000000000 @@ -86,3 +147,7 @@ start_height = 71525 [[burnchain.epochs]] epoch_name = "3.3" start_height = 109280 + +[[burnchain.epochs]] +epoch_name = "3.4" +start_height = 159645 diff --git a/sample/conf/testnet-signer.toml b/sample/conf/testnet-signer.toml index e948e5eac37..616fdcc5509 100644 --- a/sample/conf/testnet-signer.toml +++ b/sample/conf/testnet-signer.toml @@ -1,29 +1,56 @@ +# ============================================================ +# STACKS SIGNER NODE - TESTNET CONFIGURATION +# ============================================================ +# +# This configures the stacks-node to work with a signer on testnet. +# This is the NODE-SIDE config. For the signer binary config, see +# signer/mainnet-signer-conf.toml (or create a testnet variant). +# +# Key coordination points between this config and the signer binary: +# - [[events_observer]] endpoint must match signer's `endpoint` +# - [connection_options] auth_token must match signer's `auth_password` + [node] # working_dir = "/dir/to/save/chainstate" # defaults to: /tmp/stacks-node-[0-9]* rpc_bind = "0.0.0.0:20443" p2p_bind = "0.0.0.0:20444" bootstrap_node = "029266faff4c8e0ca4f934f34996a96af481df94a89b0c9bd515f3536a95682ddc@seed.testnet.hiro.so:30444" -prometheus_bind = "0.0.0.0:9153" +prometheus_bind = "0.0.0.0:9153" +stacker = true [burnchain] mode = "krypton" peer_host = "bitcoin.regtest.hiro.so" +rpc_port = 18443 peer_port = 18444 pox_prepare_length = 100 pox_reward_length = 900 -# Used for sending events to a local stacks-blockchain-api service +# Signer event observer (REQUIRED). +# WARNING: endpoint must match your signer binary's `endpoint` config. +[[events_observer]] +endpoint = "127.0.0.1:30000" +events_keys = ["stackerdb", "block_proposal", "burn_blocks"] + +# Optional: API event observer for stacks-blockchain-api service # [[events_observer]] # endpoint = "localhost:3700" # events_keys = ["*"] # timeout_ms = 60_000 -[[events_observer]] -endpoint = "127.0.0.1:30000" -events_keys = ["stackerdb", "block_proposal", "burn_blocks"] - [connection_options] -auth_token = "" # fill with a unique password +# WARNING: Must match the signer binary's `auth_password`. +auth_token = "" + +# Maximum age of block proposals accepted by this node. +# Default: 600 +# Units: seconds +# block_proposal_max_age_secs = 600 + +# Timeout for block proposal validation. +# Default: 60 +# Units: seconds +# block_proposal_validation_timeout_secs = 60 [[ustx_balance]] address = "ST2QKZ4FKHAH1NQKYKYAYZPY440FEPK7GZ1R5HBP2" @@ -88,3 +115,7 @@ start_height = 71525 [[burnchain.epochs]] epoch_name = "3.3" start_height = 109280 + +[[burnchain.epochs]] +epoch_name = "3.4" +start_height = 159645 diff --git a/stacks-common/src/libcommon.rs b/stacks-common/src/libcommon.rs index 6d2fb674d80..0ee64bd2ae2 100644 --- a/stacks-common/src/libcommon.rs +++ b/stacks-common/src/libcommon.rs @@ -92,7 +92,7 @@ pub mod consts { /// this should be updated to the latest network epoch version supported by /// this node. this will be checked by the `validate_epochs()` method. - pub const PEER_NETWORK_EPOCH: u32 = PEER_VERSION_EPOCH_3_3 as u32; + pub const PEER_NETWORK_EPOCH: u32 = PEER_VERSION_EPOCH_3_4 as u32; /// set the fourth byte of the peer version pub const PEER_VERSION_MAINNET: u32 = PEER_VERSION_MAINNET_MAJOR | PEER_NETWORK_EPOCH; diff --git a/stacks-common/src/types/mod.rs b/stacks-common/src/types/mod.rs index c12c5f17914..654686c94d7 100644 --- a/stacks-common/src/types/mod.rs +++ b/stacks-common/src/types/mod.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2024 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -461,7 +461,7 @@ impl StacksEpochId { /// Highest epoch enabled in release builds. /// Keep this in sync with `versions.toml` and `PEER_NETWORK_EPOCH` /// (validated in tests and `validate_epochs()`) - pub const RELEASE_LATEST_EPOCH: StacksEpochId = StacksEpochId::Epoch33; + pub const RELEASE_LATEST_EPOCH: StacksEpochId = StacksEpochId::Epoch34; #[cfg(any(test, feature = "testing"))] pub const fn latest() -> StacksEpochId { @@ -634,6 +634,26 @@ impl StacksEpochId { self < &StacksEpochId::Epoch34 } + /// Whether or not this epoch pre-sanitizes contract variables at deploy + /// and load time, allowing variable lookups to borrow directly. + pub fn uses_pre_sanitized_variables(&self) -> bool { + match self { + StacksEpochId::Epoch10 + | StacksEpochId::Epoch20 + | StacksEpochId::Epoch2_05 + | StacksEpochId::Epoch21 + | StacksEpochId::Epoch22 + | StacksEpochId::Epoch23 + | StacksEpochId::Epoch24 + | StacksEpochId::Epoch25 + | StacksEpochId::Epoch30 + | StacksEpochId::Epoch31 + | StacksEpochId::Epoch32 + | StacksEpochId::Epoch33 => false, + StacksEpochId::Epoch34 => true, + } + } + /// What is the sortition mining commitment window for this epoch? pub fn mining_commitment_window(&self) -> u8 { MINING_COMMITMENT_WINDOW @@ -699,6 +719,12 @@ impl StacksEpochId { } } + /// Does this epoch support the post-condition enhancements from SIP-040? + /// This includes support for `Originator` mode and the `MaySend` NFT condition. + pub fn supports_sip040_post_conditions(&self) -> bool { + self >= &StacksEpochId::Epoch34 + } + /// What is the coinbase (in uSTX) to award for the given burnchain height? /// Applies prior to SIP-029 fn coinbase_reward_pre_sip029( @@ -918,6 +944,11 @@ impl StacksEpochId { self >= &StacksEpochId::Epoch34 } + /// Whether `at-block` is available in this epoch. + pub fn supports_at_block(&self) -> bool { + self < &StacksEpochId::Epoch34 + } + /// Return the network epoch associated with the StacksEpochId pub fn network_epoch(epoch: StacksEpochId) -> u8 { match epoch { diff --git a/stacks-common/src/util/log.rs b/stacks-common/src/util/log.rs index 95ccebaf7b3..4c80bfdb301 100644 --- a/stacks-common/src/util/log.rs +++ b/stacks-common/src/util/log.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use std::collections::HashMap; use std::io::Write; use std::time::{Duration, SystemTime}; use std::{env, io, thread}; @@ -269,6 +270,66 @@ fn inner_get_loglevel() -> slog::Level { lazy_static! { static ref LOGLEVEL: slog::Level = inner_get_loglevel(); + static ref COMPONENT_FILTERS: HashMap = parse_component_filters(); +} + +/// Parse STACKS_LOG env var into component-level filter map. +/// Format: "component=level,component=level" +/// Example: "net=debug,clarity=info,miner=warn" +/// Components match as substrings against module_path!() values. +fn parse_component_filters() -> HashMap { + let mut filters = HashMap::new(); + let Ok(spec) = env::var("STACKS_LOG") else { + return filters; + }; + for part in spec.split(',') { + let part = part.trim(); + if part.is_empty() { + continue; + } + let Some((component, level_str)) = part.split_once('=') else { + continue; + }; + let level = match level_str.to_lowercase().as_str() { + "trace" => slog::Level::Trace, + "debug" => slog::Level::Debug, + "info" => slog::Level::Info, + "warn" | "warning" => slog::Level::Warning, + "error" => slog::Level::Error, + "critical" | "crit" => slog::Level::Critical, + _ => continue, + }; + filters.insert(component.to_string(), level); + } + filters +} + +/// Check if a log message at the given level should be emitted for the given module. +/// If STACKS_LOG is set, matches module_path against component filters. +/// Falls back to global LOGLEVEL if no component filter matches. +pub fn is_log_enabled_for_module(module_path: &str, level: slog::Level) -> bool { + if !COMPONENT_FILTERS.is_empty() { + // Check for matching component filter (longest match wins) + let mut best_match_len = 0; + let mut matched_level = COMPONENT_FILTERS + .get("default") + .copied() + .unwrap_or(*LOGLEVEL); + + for (component, filter_level) in COMPONENT_FILTERS.iter() { + if component == "default" { + continue; + } + if module_path.contains(component.as_str()) && component.len() > best_match_len { + best_match_len = component.len(); + matched_level = *filter_level; + } + } + + level.is_at_least(matched_level) + } else { + level.is_at_least(*LOGLEVEL) + } } pub fn get_loglevel() -> slog::Level { @@ -278,8 +339,7 @@ pub fn get_loglevel() -> slog::Level { #[macro_export] macro_rules! trace { ($($arg:tt)*) => ({ - let cur_level = $crate::util::log::get_loglevel(); - if slog::Level::Trace.is_at_least(cur_level) { + if $crate::util::log::is_log_enabled_for_module(module_path!(), slog::Level::Trace) { slog::slog_trace!($crate::util::log::LOGGER, $($arg)*) } }) @@ -288,8 +348,7 @@ macro_rules! trace { #[macro_export] macro_rules! error { ($($arg:tt)*) => ({ - let cur_level = $crate::util::log::get_loglevel(); - if slog::Level::Error.is_at_least(cur_level) { + if $crate::util::log::is_log_enabled_for_module(module_path!(), slog::Level::Error) { slog::slog_error!($crate::util::log::LOGGER, $($arg)*) } }) @@ -298,8 +357,7 @@ macro_rules! error { #[macro_export] macro_rules! warn { ($($arg:tt)*) => ({ - let cur_level = $crate::util::log::get_loglevel(); - if slog::Level::Warning.is_at_least(cur_level) { + if $crate::util::log::is_log_enabled_for_module(module_path!(), slog::Level::Warning) { slog::slog_warn!($crate::util::log::LOGGER, $($arg)*) } }) @@ -308,8 +366,7 @@ macro_rules! warn { #[macro_export] macro_rules! info { ($($arg:tt)*) => ({ - let cur_level = $crate::util::log::get_loglevel(); - if slog::Level::Info.is_at_least(cur_level) { + if $crate::util::log::is_log_enabled_for_module(module_path!(), slog::Level::Info) { slog::slog_info!($crate::util::log::LOGGER, $($arg)*) } }) @@ -318,8 +375,7 @@ macro_rules! info { #[macro_export] macro_rules! debug { ($($arg:tt)*) => ({ - let cur_level = $crate::util::log::get_loglevel(); - if slog::Level::Debug.is_at_least(cur_level) { + if $crate::util::log::is_log_enabled_for_module(module_path!(), slog::Level::Debug) { slog::slog_debug!($crate::util::log::LOGGER, $($arg)*) } }) @@ -328,8 +384,7 @@ macro_rules! debug { #[macro_export] macro_rules! fatal { ($($arg:tt)*) => ({ - let cur_level = $crate::util::log::get_loglevel(); - if slog::Level::Critical.is_at_least(cur_level) { + if $crate::util::log::is_log_enabled_for_module(module_path!(), slog::Level::Critical) { slog::slog_crit!($crate::util::log::LOGGER, $($arg)*) } }) @@ -356,4 +411,45 @@ mod tests { slog::slog_warn!(logger, "Warn test"); //equivalent to warn!(..) slog::slog_error!(logger, "Erro test"); //equivalent to erro!(..) } + + #[test] + fn test_parse_component_filters_empty() { + let filters = parse_component_filters(); + // Without STACKS_LOG set, returns empty map + // (can't test env-dependent behavior in unit tests without side effects) + assert!(filters.is_empty() || !filters.is_empty()); + } + + #[test] + fn test_is_log_enabled_default_behavior() { + // Without component filters, falls back to global level + // Info should be enabled at Info level + assert!(is_log_enabled_for_module("stackslib::net::download", slog::Level::Info)); + // Error should always be enabled + assert!(is_log_enabled_for_module("stackslib::net::download", slog::Level::Error)); + } + + #[test] + fn test_parse_component_filters_format() { + // Test the parsing logic directly + let mut filters = HashMap::new(); + let spec = "net=debug,clarity=info,miner=warn"; + for part in spec.split(',') { + if let Some((component, level_str)) = part.split_once('=') { + let level = match level_str { + "trace" => slog::Level::Trace, + "debug" => slog::Level::Debug, + "info" => slog::Level::Info, + "warn" => slog::Level::Warning, + "error" => slog::Level::Error, + _ => continue, + }; + filters.insert(component.to_string(), level); + } + } + assert_eq!(filters.len(), 3); + assert_eq!(filters.get("net"), Some(&slog::Level::Debug)); + assert_eq!(filters.get("clarity"), Some(&slog::Level::Info)); + assert_eq!(filters.get("miner"), Some(&slog::Level::Warning)); + } } diff --git a/stacks-common/src/util/secp256r1.rs b/stacks-common/src/util/secp256r1.rs index 37507102df1..f22a06dac44 100644 --- a/stacks-common/src/util/secp256r1.rs +++ b/stacks-common/src/util/secp256r1.rs @@ -94,6 +94,19 @@ impl MessageSignature { pub fn to_p256_signature(&self) -> Result { P256Signature::from_slice(&self.0).map_err(|_| Secp256r1Error::InvalidSignature) } + + /// Returns a high-S version of this signature by negating S (s' = -s mod n). + /// If the signature is already high-S, it is returned unchanged. + #[cfg(any(test, feature = "testing"))] + pub fn to_high_s(&self) -> Result { + let p256_sig = self.to_p256_signature()?; + // Normalize to low-S first, then negate to get high-S + let low_sig = p256_sig.normalize_s().unwrap_or(p256_sig); + let (r, s) = (low_sig.r(), low_sig.s()); + let high_sig = + P256Signature::from_scalars(*r, -(*s)).map_err(|_| Secp256r1Error::InvalidSignature)?; + Ok(MessageSignature::from_p256_signature(&high_sig)) + } } impl Secp256r1PublicKey { diff --git a/stacks-node/src/event_dispatcher.rs b/stacks-node/src/event_dispatcher.rs index 69b401a88bd..01f318d8219 100644 --- a/stacks-node/src/event_dispatcher.rs +++ b/stacks-node/src/event_dispatcher.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2025 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -92,6 +92,7 @@ enum EventDispatcherError { SerializationError(serde_json::Error), HttpError(std::io::Error), DbError(stacks::util_lib::db::Error), + UrlParseError(String), } impl fmt::Display for EventDispatcherError { @@ -100,6 +101,7 @@ impl fmt::Display for EventDispatcherError { EventDispatcherError::SerializationError(ref e) => fmt::Display::fmt(e, f), EventDispatcherError::HttpError(ref e) => fmt::Display::fmt(e, f), EventDispatcherError::DbError(ref e) => fmt::Display::fmt(e, f), + EventDispatcherError::UrlParseError(ref s) => write!(f, "URL parse error: {s}"), } } } @@ -110,6 +112,7 @@ impl core::error::Error for EventDispatcherError { EventDispatcherError::SerializationError(ref e) => Some(e), EventDispatcherError::HttpError(ref e) => Some(e), EventDispatcherError::DbError(ref e) => Some(e), + EventDispatcherError::UrlParseError(_) => None, } } } @@ -479,20 +482,21 @@ impl EventDispatcher { fn create_dispatch_matrix_and_event_vector<'a>( &self, receipts: &'a [StacksTransactionReceipt], - ) -> ( - Vec>, - Vec<(bool, Txid, &'a StacksTransactionEvent)>, - ) { + ) -> (Vec>, Vec<(Txid, &'a StacksTransactionEvent)>) { let mut dispatch_matrix: Vec> = self .registered_observers .iter() .map(|_| HashSet::new()) .collect(); - let mut events: Vec<(bool, Txid, &StacksTransactionEvent)> = vec![]; + let mut events: Vec<(Txid, &StacksTransactionEvent)> = vec![]; let mut i: usize = 0; for receipt in receipts { let tx_hash = receipt.transaction.txid(); + if receipt.post_condition_aborted { + debug!("Transaction {tx_hash} aborted by post-condition, skipping events"); + continue; + } for event in receipt.events.iter() { match event { StacksTransactionEvent::SmartContractEvent(event_data) => { @@ -557,7 +561,7 @@ impl EventDispatcher { ); } } - events.push((!receipt.post_condition_aborted, tx_hash.clone(), event)); + events.push((tx_hash.clone(), event)); for o_i in &self.any_event_observers_lookup { dispatch_matrix[*o_i as usize].insert(i); } @@ -1081,36 +1085,49 @@ impl EventDispatcher { "Event dispatcher: processing pending payload: {}", request_data.url ); - let full_url = Url::parse(request_data.url.as_str()).unwrap_or_else(|_| { - panic!( - "Event dispatcher: unable to parse {} as a URL", - request_data.url - ) - }); + let full_url = match Url::parse(request_data.url.as_str()) { + Ok(url) => url, + Err(e) => { + error!( + "Event dispatcher: unable to parse pending URL, skipping"; + "url" => &request_data.url, + "error" => %e + ); + if let Err(e) = conn.delete_payload(id) { + error!( + "Event observer: failed to delete invalid pending payload"; + "error" => ?e + ); + } + continue; + } + }; // find the right observer let observer = self.registered_observers.iter().find(|observer| { - let endpoint_url = Url::parse(format!("http://{}", &observer.endpoint).as_str()) - .unwrap_or_else(|_| { - panic!( - "Event dispatcher: unable to parse {} as a URL", - observer.endpoint - ) - }); + let endpoint_url = + match Url::parse(format!("http://{}", &observer.endpoint).as_str()) { + Ok(url) => url, + Err(e) => { + warn!( + "Event dispatcher: unable to parse observer endpoint"; + "endpoint" => &observer.endpoint, + "error" => %e + ); + return false; + } + }; full_url.origin() == endpoint_url.origin() }); let Some(observer) = observer else { - // This observer is no longer registered, skip and delete + // No matching observer found. This could be because the observer was + // removed from config, or because the endpoint URL failed to parse. + // Keep the payload in DB rather than deleting — it will be retried on + // next restart when the observer may be reconfigured correctly. info!( - "Event dispatcher: observer {} no longer registered, skipping", - request_data.url + "Event dispatcher: no matching observer for pending payload, keeping for retry"; + "url" => &request_data.url ); - if let Err(e) = conn.delete_payload(id) { - error!( - "Event observer: failed to delete pending payload from database"; - "error" => ?e - ); - } continue; }; @@ -1158,10 +1175,19 @@ impl EventDispatcher { "Event dispatcher: Sending payload"; "url" => &data.url, "bytes" => data.payload_bytes.len() ); - let url = Url::parse(&data.url) - .unwrap_or_else(|_| panic!("Event dispatcher: unable to parse {} as a URL", data.url)); + let url = Url::parse(&data.url).map_err(|e| { + error!( + "Event dispatcher: unable to parse URL"; + "url" => &data.url, + "error" => %e + ); + EventDispatcherError::UrlParseError(data.url.clone()) + })?; - let host = url.host_str().expect("Invalid URL: missing host"); + let host = url.host_str().ok_or_else(|| { + error!("Event dispatcher: URL missing host"; "url" => %url); + EventDispatcherError::UrlParseError(format!("missing host in URL: {url}")) + })?; let port = url.port_or_known_default().unwrap_or(80); let peerhost: PeerHost = format!("{host}:{port}") .parse() @@ -1169,17 +1195,30 @@ impl EventDispatcher { let mut backoff = Duration::from_millis(100); let mut attempts: i32 = 0; + let max_attempts: i32 = 25; // Cap the backoff at 3x the timeout let max_backoff = data.timeout.saturating_mul(3); loop { - let mut request = StacksHttpRequest::new_for_peer( + let mut request = match StacksHttpRequest::new_for_peer( peerhost.clone(), "POST".into(), url.path().into(), HttpRequestContents::new().payload_json_bytes(Arc::clone(&data.payload_bytes)), - ) - .unwrap_or_else(|_| panic!("FATAL: failed to encode infallible data as HTTP request")); + ) { + Ok(req) => req, + Err(e) => { + error!( + "Event dispatcher: failed to encode HTTP request"; + "url" => %url, + "error" => %e + ); + return Err(EventDispatcherError::HttpError(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("failed to encode HTTP request: {e}"), + ))); + } + }; request.add_header("Connection".into(), "close".into()); match send_http_request(host, port, request, data.timeout) { Ok(response) => { @@ -1214,13 +1253,26 @@ impl EventDispatcher { } } + attempts = attempts.saturating_add(1); + if attempts >= max_attempts { + error!( + "Event dispatcher: giving up after max retries"; + "url" => %url, + "attempts" => attempts, + "max_attempts" => max_attempts + ); + return Err(EventDispatcherError::HttpError(std::io::Error::new( + std::io::ErrorKind::TimedOut, + format!("event observer at {url} unreachable after {attempts} attempts"), + ))); + } + sleep(backoff); let jitter: u64 = rand::thread_rng().gen_range(0..100); backoff = std::cmp::min( backoff.saturating_mul(2) + Duration::from_millis(jitter), max_backoff, ); - attempts = attempts.saturating_add(1); } Ok(()) @@ -1259,8 +1311,7 @@ impl EventDispatcher { ) { let http_result = Self::make_http_request(data, disable_retries); - if let Err(err) = http_result { - // log but continue + if let Err(ref err) = http_result { error!("EventDispatcher: dispatching failed"; "url" => data.url.clone(), "error" => ?err); } @@ -1270,11 +1321,17 @@ impl EventDispatcher { return; } - // We're deleting regardless of result -- if retries are disabled, that means - // we're supposed to forget about it in case of failure. If they're not disabled, - // then we wouldn't be here in case of failue, because `make_http_request` retries - // until it's successful (with the exception of the above fault injection which - // simulates a shutdown). + // Only delete if the request succeeded or if retries are disabled (fire-and-forget mode). + // If make_http_request exhausted retries, keep the event in the DB so + // retry_pending_payloads can pick it up on restart. + if http_result.is_err() && !disable_retries { + warn!( + "Event dispatcher: keeping event in DB for retry on restart"; + "url" => &data.url + ); + return; + } + let deletion_result = self.delete_from_db(id); if let Err(e) = deletion_result { @@ -1305,7 +1362,7 @@ impl EventDispatcher { &self, event_observer: &EventObserver, parent_index_block_hash: &StacksBlockId, - filtered_events: &[(usize, &(bool, Txid, &StacksTransactionEvent))], + filtered_events: &[(usize, &(Txid, &StacksTransactionEvent))], serialized_txs: &[TransactionEventPayload], burn_block_hash: &BurnchainHeaderHash, burn_block_height: u32, @@ -1314,10 +1371,10 @@ impl EventDispatcher { // Serialize events to JSON let serialized_events: Vec = filtered_events .iter() - .map(|(event_index, (committed, txid, event))| { - event - .json_serialize(*event_index, txid, *committed) - .unwrap() + .map(|(event_index, (txid, event))| { + // Since we no longer send events for post condition aborted transactions, + // all events we serialize here are committed events, so we can set the `committed` field to `true`. + event.json_serialize(*event_index, txid, true).unwrap() }) .collect(); diff --git a/stacks-node/src/event_dispatcher/payloads.rs b/stacks-node/src/event_dispatcher/payloads.rs index 72dccd2b316..d3dbc3433f7 100644 --- a/stacks-node/src/event_dispatcher/payloads.rs +++ b/stacks-node/src/event_dispatcher/payloads.rs @@ -316,7 +316,7 @@ pub fn make_new_attachment_payload( #[allow(clippy::too_many_arguments)] pub fn make_new_block_processed_payload( - filtered_events: Vec<(usize, &(bool, Txid, &StacksTransactionEvent))>, + filtered_events: Vec<(usize, &(Txid, &StacksTransactionEvent))>, block: &StacksBlockEventData, metadata: &StacksHeaderInfo, receipts: &[StacksTransactionReceipt], @@ -337,10 +337,10 @@ pub fn make_new_block_processed_payload( // Serialize events to JSON let serialized_events: Vec = filtered_events .iter() - .map(|(event_index, (committed, txid, event))| { - event - .json_serialize(*event_index, txid, *committed) - .unwrap() + .map(|(event_index, (txid, event))| { + // Since we no longer send events for post condition aborted transactions, + // all events we serialize here are committed events, so we can set the `committed` field to `true`. + event.json_serialize(*event_index, txid, true).unwrap() }) .collect(); diff --git a/stacks-node/src/event_dispatcher/tests.rs b/stacks-node/src/event_dispatcher/tests.rs index 52f03062913..dd819567f84 100644 --- a/stacks-node/src/event_dispatcher/tests.rs +++ b/stacks-node/src/event_dispatcher/tests.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2025 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -22,7 +22,7 @@ use clarity::boot_util::boot_code_id; use clarity::vm::costs::ExecutionCost; use clarity::vm::events::SmartContractEventData; use clarity::vm::types::StacksAddressExtensions; -use clarity::vm::Value; +use clarity::vm::{ClarityName, ContractName, Value}; use rusqlite::Connection; use serial_test::serial; use stacks::address::{AddressHashMode, C32_ADDRESS_VERSION_TESTNET_SINGLESIG}; @@ -32,11 +32,13 @@ use stacks::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader}; use stacks::chainstate::stacks::db::{StacksBlockHeaderTypes, StacksHeaderInfo}; use stacks::chainstate::stacks::events::{StacksBlockEventData, TransactionOrigin}; use stacks::chainstate::stacks::{ - SinglesigHashMode, SinglesigSpendingCondition, StacksBlock, TenureChangeCause, - TenureChangePayload, TokenTransferMemo, TransactionAnchorMode, TransactionAuth, - TransactionPayload, TransactionPostConditionMode, TransactionPublicKeyEncoding, - TransactionSpendingCondition, TransactionVersion, + SinglesigHashMode, SinglesigSpendingCondition, StacksBlock, StacksTransactionSigner, + TenureChangeCause, TenureChangePayload, TokenTransferMemo, TransactionAnchorMode, + TransactionAuth, TransactionContractCall, TransactionPayload, TransactionPostConditionMode, + TransactionPublicKeyEncoding, TransactionSpendingCondition, TransactionVersion, }; +use stacks::core::test_util::{make_unsigned_tx, to_addr}; +use stacks::core::CHAIN_ID_TESTNET; use stacks::types::chainstate::{ BlockHeaderHash, StacksAddress, StacksPrivateKey, StacksPublicKey, }; @@ -50,6 +52,110 @@ use tiny_http::{Method, Response, Server, StatusCode}; use crate::event_dispatcher::payloads::*; use crate::event_dispatcher::*; +#[test] +fn test_post_condition_aborted_transaction_does_not_emit_events() { + // Create a transaction receipt with post_condition_aborted = true and a dummy event + let tx = { + let private_key = StacksPrivateKey::from_seed("PostConditionFailure".as_bytes()); + let addr = to_addr(&private_key); + + let contract_name = ContractName::from("test"); + let function_name = ClarityName::from("test"); + + let payload = TransactionContractCall { + address: addr.clone(), + contract_name, + function_name, + function_args: vec![], + }; + let mut unsigned_tx = make_unsigned_tx( + TransactionPayload::ContractCall(payload), + &private_key, + None, + 1, + None, + 1000, + CHAIN_ID_TESTNET, + TransactionAnchorMode::Any, + TransactionVersion::Testnet, + ); + unsigned_tx.post_condition_mode = TransactionPostConditionMode::Deny; + + let mut tx_signer = StacksTransactionSigner::new(&unsigned_tx); + tx_signer.sign_origin(&private_key).unwrap(); + tx_signer.get_tx().unwrap() + }; + let txid = tx.txid(); + let mut receipt = StacksTransactionReceipt { + transaction: TransactionOrigin::Stacks(tx), + events: vec![StacksTransactionEvent::SmartContractEvent( + SmartContractEventData { + key: ( + clarity::boot_util::boot_code_id("dummy", false), + "dummy".into(), + ), + value: Value::Bool(true), + }, + )], + post_condition_aborted: true, + result: Value::okay_true(), + stx_burned: 0, + contract_analysis: None, + execution_cost: ExecutionCost::ZERO, + microblock_header: None, + tx_index: 0, + vm_error: None, + }; + + let receipts = vec![receipt.clone()]; + + // Set up a dispatcher with a dummy observer + let dir = tempfile::tempdir().unwrap(); + let mut dispatcher = EventDispatcher::new(dir.path().to_path_buf()); + dispatcher.register_observer(&EventObserverConfig { + endpoint: "dummy-endpoint".to_string(), + events_keys: vec![EventKeyType::AnyEvent], + timeout_ms: 1000, + disable_retries: true, + }); + + // Call create_dispatch_matrix_and_event_vector with the aborted receipt + let (dispatch_matrix, events) = dispatcher.create_dispatch_matrix_and_event_vector(&receipts); + + // There should be no events emitted for post-condition aborted transactions + assert!( + events.is_empty(), + "No events should be emitted for post-condition aborted transactions" + ); + for observer_events in dispatch_matrix { + assert!( + observer_events.is_empty(), + "No observer should receive events for post-condition aborted transactions" + ); + } + + receipt.post_condition_aborted = false; + let receipts = vec![receipt]; + // Call create_dispatch_matrix_and_event_vector with a successful receipt + let (dispatch_matrix, events) = dispatcher.create_dispatch_matrix_and_event_vector(&receipts); + + // There should be events emitted for successful transactions + assert_eq!( + events.len(), + 1, + "Events should be emitted for successful transactions" + ); + + assert_eq!(events.first().unwrap().0, txid); + for observer_events in dispatch_matrix { + assert_eq!( + observer_events.len(), + 1, + "Observers should receive events for successful transactions" + ); + } +} + #[test] fn build_block_processed_event() { let filtered_events = vec![]; diff --git a/stacks-node/src/nakamoto_node/relayer.rs b/stacks-node/src/nakamoto_node/relayer.rs index 925a8d330d9..828e5790849 100644 --- a/stacks-node/src/nakamoto_node/relayer.rs +++ b/stacks-node/src/nakamoto_node/relayer.rs @@ -1702,7 +1702,7 @@ impl RelayerThread { #[cfg(test)] fn fault_injection_skip_block_commit(&self) -> bool { - self.globals.counters.naka_skip_commit_op.get() + self.globals.counters.skip_commit_op.get() } #[cfg(not(test))] @@ -2077,6 +2077,34 @@ impl RelayerThread { } } + /// Check available disk space on the working directory partition. + /// Logs warnings at low thresholds and initiates shutdown if critically low. + fn check_disk_space(&self) { + let working_dir = &self.config.node.working_dir; + let output = match std::process::Command::new("df") + .args(["-B1", "--output=avail", working_dir.as_str()]) + .output() + { + Ok(o) => o, + Err(_) => return, // df not available (non-Linux), skip silently + }; + let stdout = String::from_utf8_lossy(&output.stdout); + let avail_bytes: u64 = match stdout.lines().nth(1).and_then(|l| l.trim().parse().ok()) { + Some(b) => b, + None => return, + }; + + let gb = avail_bytes / (1024 * 1024 * 1024); + if avail_bytes < 1_000_000_000 { + error!("Disk space critically low, initiating shutdown"; "available_gb" => gb, "path" => working_dir.as_str()); + self.globals.signal_stop(); + } else if avail_bytes < 5_000_000_000 { + error!("Disk space critical"; "available_gb" => gb, "path" => working_dir.as_str()); + } else if avail_bytes < 20_000_000_000 { + warn!("Disk space low"; "available_gb" => gb, "path" => working_dir.as_str()); + } + } + /// Main loop of the relayer. /// Runs in a separate thread. /// Continuously receives from `relay_rcv`. @@ -2089,8 +2117,15 @@ impl RelayerThread { // how often we perform a loop pass below let poll_frequency_ms = 1_000; + let mut disk_check_counter: u64 = 0; + let disk_check_interval: u64 = 60; // check every 60 seconds while self.globals.keep_running() { + // Periodic disk space check + disk_check_counter += 1; + if disk_check_counter % disk_check_interval == 0 { + self.check_disk_space(); + } self.check_tenure_timers(); let raised_initiative = self.globals.take_initiative(); let timed_out = Instant::now() >= self.next_initiative; diff --git a/stacks-node/src/neon_node.rs b/stacks-node/src/neon_node.rs index 6b4817f35d7..d82967afc6b 100644 --- a/stacks-node/src/neon_node.rs +++ b/stacks-node/src/neon_node.rs @@ -2645,6 +2645,11 @@ impl BlockMinerThread { ); // let's commit + #[cfg(test)] + if self.globals.counters.skip_commit_op.get() { + debug!("Relayer: fault injection: skip block commit"); + return None; + } let op = self.make_block_commit( &mut burn_db, &mut chain_state, diff --git a/stacks-node/src/run_loop/neon.rs b/stacks-node/src/run_loop/neon.rs index 9c3b330e1d4..7df3cd977fb 100644 --- a/stacks-node/src/run_loop/neon.rs +++ b/stacks-node/src/run_loop/neon.rs @@ -135,7 +135,7 @@ pub struct Counters { pub naka_miner_current_rejections_timeout_secs: RunLoopCounter, #[cfg(test)] - pub naka_skip_commit_op: TestFlag, + pub skip_commit_op: TestFlag, } impl Counters { diff --git a/stacks-node/src/tests/nakamoto_integrations.rs b/stacks-node/src/tests/nakamoto_integrations.rs index 5e03ffa5896..8e6ef772988 100644 --- a/stacks-node/src/tests/nakamoto_integrations.rs +++ b/stacks-node/src/tests/nakamoto_integrations.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -63,10 +63,12 @@ use stacks::chainstate::stacks::miner::{ TEST_TX_STALL, }; use stacks::chainstate::stacks::{ - SinglesigHashMode, SinglesigSpendingCondition, StacksTransaction, TenureChangeCause, - TenureChangePayload, TransactionAnchorMode, TransactionAuth, TransactionContractCall, - TransactionPayload, TransactionPostConditionMode, TransactionPublicKeyEncoding, - TransactionSmartContract, TransactionSpendingCondition, TransactionVersion, MAX_BLOCK_LEN, + AssetInfo, FungibleConditionCode, NonfungibleConditionCode, PostConditionPrincipal, + SinglesigHashMode, SinglesigSpendingCondition, StacksTransaction, StacksTransactionSigner, + TenureChangeCause, TenureChangePayload, TransactionAnchorMode, TransactionAuth, + TransactionContractCall, TransactionPayload, TransactionPostCondition, + TransactionPostConditionMode, TransactionPublicKeyEncoding, TransactionSmartContract, + TransactionSpendingCondition, TransactionVersion, MAX_BLOCK_LEN, }; use stacks::config::{EventKeyType, InitialBalance}; use stacks::core::mempool::{MemPoolWalkStrategy, MAXIMUM_MEMPOOL_TX_CHAINING}; @@ -333,8 +335,12 @@ pub fn check_nakamoto_empty_block_heuristics(mainnet: bool) { tx.payload, TransactionPayload::TenureChange(_) | TransactionPayload::Coinbase(..) ) { - error!("Nakamoto TenureChange(BlockFound) block should only have coinbase and tenure change txs, but found tx: {tx:?}"); - panic!("Nakamoto TenureChange(BlockFound) block should only have coinbase and tenure change txs"); + error!( + "Nakamoto TenureChange(BlockFound) block should only have coinbase and tenure change txs, but found tx: {tx:?}" + ); + panic!( + "Nakamoto TenureChange(BlockFound) block should only have coinbase and tenure change txs" + ); } } } @@ -1249,7 +1255,9 @@ pub fn boot_to_pre_epoch_3_boundary( naka_conf, ); - info!("Bootstrapped to one block before Epoch 3.0 boundary, Epoch 2.x miner should continue for one more block"); + info!( + "Bootstrapped to one block before Epoch 3.0 boundary, Epoch 2.x miner should continue for one more block" + ); } fn get_signer_index( @@ -1286,7 +1294,9 @@ pub fn get_key_for_cycle( ) -> Result>, String> { let client = reqwest::blocking::Client::new(); let boot_address = StacksAddress::burn_address(is_mainnet); - let path = format!("http://{http_origin}/v2/contracts/call-read/{boot_address}/signers-voting/get-approved-aggregate-key"); + let path = format!( + "http://{http_origin}/v2/contracts/call-read/{boot_address}/signers-voting/get-approved-aggregate-key" + ); let body = CallReadOnlyRequestBody { sender: boot_address.to_string(), sponsor: None, @@ -1471,7 +1481,9 @@ pub fn boot_to_epoch_3_reward_set_calculation_boundary( naka_conf, ); - info!("Bootstrapped to Epoch 3.0 reward set calculation boundary height: {epoch_3_reward_set_calculation_boundary}."); + info!( + "Bootstrapped to Epoch 3.0 reward set calculation boundary height: {epoch_3_reward_set_calculation_boundary}." + ); } /// @@ -1556,6 +1568,86 @@ pub fn wait_for_first_naka_block_commit( } } +#[allow(clippy::too_many_arguments)] +fn make_contract_call_with_post_conditions( + sender: &StacksPrivateKey, + nonce: u64, + tx_fee: u64, + chain_id: u32, + contract_addr: &StacksAddress, + contract_name: &str, + function_name: &str, + function_args: &[Value], + post_condition_mode: TransactionPostConditionMode, + post_conditions: Vec, +) -> Vec { + let auth = TransactionAuth::from_p2pkh(sender).unwrap(); + let payload = TransactionPayload::new_contract_call( + contract_addr.clone(), + contract_name, + function_name, + function_args.to_vec(), + ) + .unwrap(); + + let mut tx = StacksTransaction::new(TransactionVersion::Testnet, auth, payload); + tx.chain_id = chain_id; + tx.set_tx_fee(tx_fee); + tx.set_origin_nonce(nonce); + tx.post_condition_mode = post_condition_mode; + tx.post_conditions = post_conditions; + + let mut signer = StacksTransactionSigner::new(&tx); + signer.sign_origin(sender).unwrap(); + signer.get_tx().unwrap().serialize_to_vec() +} + +fn get_tx_result_by_id(txid: &str) -> Option { + for block in test_observer::get_blocks().iter() { + for tx in block.get("transactions").unwrap().as_array().unwrap() { + let Some(observed_txid) = tx + .get("txid") + .and_then(|v| v.as_str()) + .and_then(|v| v.strip_prefix("0x")) + else { + continue; + }; + if observed_txid == txid { + let Some(raw_result) = tx + .get("raw_result") + .and_then(|v| v.as_str()) + .and_then(|v| v.strip_prefix("0x")) + else { + continue; + }; + return Value::try_deserialize_hex_untyped(raw_result).ok(); + } + } + } + None +} + +fn get_tx_status_by_id(txid: &str) -> Option { + for block in test_observer::get_blocks().iter() { + for tx in block.get("transactions").unwrap().as_array().unwrap() { + let Some(observed_txid) = tx + .get("txid") + .and_then(|v| v.as_str()) + .and_then(|v| v.strip_prefix("0x")) + else { + continue; + }; + if observed_txid == txid { + return tx + .get("status") + .and_then(|v| v.as_str()) + .map(str::to_string); + } + } + } + None +} + // Check for missing burn blocks in `range`, but allow for a missed block at // the epoch 3 transition. Panic if any other blocks are missing. fn check_nakamoto_no_missing_blocks(conf: &Config, range: impl RangeBounds) { @@ -1692,7 +1784,11 @@ fn simple_neon_integration() { .unwrap(); } let post_commits = node_counters.naka_submitted_commits.load(Ordering::SeqCst); - assert_eq!(prior_commits + 15, post_commits, "There should have been exactly {tenures_count} submitted commits during the {tenures_count} tenures"); + assert_eq!( + prior_commits + 15, + post_commits, + "There should have been exactly {tenures_count} submitted commits during the {tenures_count} tenures" + ); // Submit a TX let transfer_tx = make_stacks_transfer_serialized( @@ -2265,7 +2361,9 @@ fn flash_blocks_on_epoch_3_FLAKY() { check_nakamoto_empty_block_heuristics(naka_conf.is_mainnet()); info!("Verified burn block ranges, including expected gap for flash blocks"); - info!("Confirmed that the gap includes the Epoch 3.0 activation height (Bitcoin block height): {epoch_3_start_height}"); + info!( + "Confirmed that the gap includes the Epoch 3.0 activation height (Bitcoin block height): {epoch_3_start_height}" + ); coord_channel .lock() @@ -3044,7 +3142,10 @@ fn correct_burn_outs() { // For cycles in or after first_epoch_3_cycle, ensure signers are present let signers = reward_set["signers"].as_array().unwrap(); - assert!(!signers.is_empty(), "Signers should be set in any epoch-3 cycles. First epoch-3 cycle: {first_epoch_3_cycle}. Checked cycle number: {cycle_number}"); + assert!( + !signers.is_empty(), + "Signers should be set in any epoch-3 cycles. First epoch-3 cycle: {first_epoch_3_cycle}. Checked cycle number: {cycle_number}" + ); assert_eq!( reward_set["rewarded_addresses"].as_array().unwrap().len(), @@ -3055,7 +3156,10 @@ fn correct_burn_outs() { // the signer should have 1 "slot", because they stacked the minimum stacking amount let signer_weight = signers[0]["weight"].as_u64().unwrap(); - assert_eq!(signer_weight, 1, "The signer should have a weight of 1, indicating they stacked the minimum stacking amount"); + assert_eq!( + signer_weight, 1, + "The signer should have a weight of 1, indicating they stacked the minimum stacking amount" + ); } check_nakamoto_empty_block_heuristics(naka_conf.is_mainnet()); @@ -5298,7 +5402,7 @@ fn bad_commit_does_not_trigger_fork() { blocks_processed, naka_submitted_commits: commits_submitted, naka_mined_blocks: mined_blocks, - naka_skip_commit_op: test_skip_commit_op, + skip_commit_op: test_skip_commit_op, .. } = run_loop.counters(); let counters = run_loop.counters(); @@ -5394,7 +5498,9 @@ fn bad_commit_does_not_trigger_fork() { thread::sleep(Duration::from_secs(1)); } - info!("Tenure B broadcasted but did not process a block. Issue the next bitcoin block and unstall block commits."); + info!( + "Tenure B broadcasted but did not process a block. Issue the next bitcoin block and unstall block commits." + ); // the block will be stored, not processed, so load it out of staging let tip_sn = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()) @@ -5671,8 +5777,7 @@ fn check_block_heights() { // Deploy this version with the Clarity 1 / 2 before epoch 3 let contract0_name = "test-contract-0"; - let contract_clarity1 = - "(define-read-only (get-heights) { burn-block-height: burn-block-height, block-height: block-height })"; + let contract_clarity1 = "(define-read-only (get-heights) { burn-block-height: burn-block-height, block-height: block-height })"; let contract_tx0 = make_contract_publish( &sender_sk, @@ -5783,8 +5888,7 @@ fn check_block_heights() { // This version uses the Clarity 3 keywords let contract3_name = "test-contract-3"; - let contract_clarity3 = - "(define-read-only (get-heights) { burn-block-height: burn-block-height, stacks-block-height: stacks-block-height, tenure-height: tenure-height })"; + let contract_clarity3 = "(define-read-only (get-heights) { burn-block-height: burn-block-height, stacks-block-height: stacks-block-height, tenure-height: tenure-height })"; let contract_tx3 = make_contract_publish( &sender_sk, @@ -6082,7 +6186,7 @@ fn nakamoto_attempt_time() { privk: Secp256k1PrivateKey, _address: StacksAddress, } - let num_accounts = 1_000; + let num_accounts = 100; let init_account_balance = 1_000_000_000; let account_keys = add_initial_balances(&mut naka_conf, num_accounts, init_account_balance); let mut account = account_keys @@ -6298,7 +6402,7 @@ fn nakamoto_attempt_time() { .expect("Mutex poisoned") .get_stacks_blocks_processed(); - let tx_limit = 10000; + let tx_limit = 1000; let tx_fee = 500; let amount = 500; let mut tx_total_size = 0; @@ -7729,6 +7833,19 @@ fn check_block_times() { let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None); let http_origin = format!("http://{}", &naka_conf.node.rpc_bind); naka_conf.burnchain.chain_id = CHAIN_ID_TESTNET + 1; + // Keep this test in Epoch 3.3 so `at-block` remains available. + { + let epochs = naka_conf + .burnchain + .epochs + .as_mut() + .expect("Missing burnchain epochs in config"); + epochs.truncate_after(StacksEpochId::Epoch33); + epochs + .get_mut(StacksEpochId::Epoch33) + .expect("Missing epoch 3.3 in config") + .end_height = STACKS_EPOCH_MAX; + } let sender_sk = Secp256k1PrivateKey::random(); let sender_signer_sk = Secp256k1PrivateKey::random(); let sender_signer_addr = tests::to_addr(&sender_signer_sk); @@ -8614,7 +8731,10 @@ fn check_block_info() { .unwrap() .is_none()); - assert_eq!(c3_interim_ti, c3_cur_tenure_ti, "Tenure info should be the same whether queried using the starting block or the interim block height"); + assert_eq!( + c3_interim_ti, c3_cur_tenure_ti, + "Tenure info should be the same whether queried using the starting block or the interim block height" + ); // c0 and c1 should have different block info data than the interim block assert_ne!(c0_cur_tenure["header-hash"], c3_interim_bi["header-hash"]); @@ -9289,7 +9409,9 @@ fn mock_mining() { submit_tx(&http_origin, &transfer_tx); // Wait for the interim block to be mock-mined - info!("Waiting for the interim block {interim_block_ix} of tenure {tenure_ix} to be mock-mined"); + info!( + "Waiting for the interim block {interim_block_ix} of tenure {tenure_ix} to be mock-mined" + ); wait_for(30, || { Ok(follower_mined_blocks.load(Ordering::SeqCst) > follower_mined_before) }) @@ -9529,7 +9651,7 @@ fn run_mock_mining_ongoing_tenure_boot_test(check_empty_sortition_recovery: bool let follower_mined_before_empty_sortition = follower_mined_blocks.load(Ordering::SeqCst); // Force an empty sortition and ensure the restarted mock miner keeps mining afterwards. - counters.naka_skip_commit_op.set(true); + counters.skip_commit_op.set(true); let miner_burn_height_before = get_chain_info(&naka_conf).burn_block_height; let follower_burn_height_before = get_chain_info(&follower_conf).burn_block_height; @@ -9556,7 +9678,7 @@ fn run_mock_mining_ongoing_tenure_boot_test(check_empty_sortition_recovery: bool }) .expect("Mock miner did not continue mining after empty sortition"); TEST_P2P_BROADCAST_STALL.set(false); - counters.naka_skip_commit_op.set(false); + counters.skip_commit_op.set(false); } else { // Confirm the restarted follower can start mining in the middle of an ongoing tenure. let follower_mined_before_mid_tenure = follower_mined_blocks.load(Ordering::SeqCst); @@ -9580,7 +9702,7 @@ fn run_mock_mining_ongoing_tenure_boot_test(check_empty_sortition_recovery: bool // Best-effort reset for test globals before teardown. TEST_P2P_BROADCAST_STALL.set(false); - counters.naka_skip_commit_op.set(false); + counters.skip_commit_op.set(false); coord_channel .lock() @@ -11251,7 +11373,7 @@ fn test_tenure_extend_from_flashblocks() { next_block_and_mine_commit(btc_regtest_controller, 60, &naka_conf, &counters).unwrap(); // prevent the miner from sending another block-commit - counters.naka_skip_commit_op.set(true); + counters.skip_commit_op.set(true); let info_before = get_chain_info(&naka_conf); @@ -11343,7 +11465,7 @@ fn test_tenure_extend_from_flashblocks() { } // unstall miner thread and allow block-commits again - counters.naka_skip_commit_op.set(false); + counters.skip_commit_op.set(false); fault_injection_unstall_miner(); // wait for the miner directive to be processed @@ -14076,8 +14198,9 @@ fn test_sip_031_last_phase_out_of_epoch() { PrincipalData::Standard(StandardPrincipalData::transient()), None, LimitedCostTracker::new_free(), - |tx| { - tx.eval_read_only( + |exec_state, invoke_ctx| { + exec_state.eval_read_only( + &invoke_ctx, &boot_code_id(SIP_031_NAME, naka_conf.is_mainnet()), "(get-recipient)", ) @@ -15333,6 +15456,19 @@ fn check_block_time_keyword() { let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None); let http_origin = format!("http://{}", &naka_conf.node.rpc_bind); naka_conf.burnchain.chain_id = CHAIN_ID_TESTNET + 1; + // Keep this test below Epoch 3.4 so `at-block` stays valid. + { + let epochs = naka_conf + .burnchain + .epochs + .as_mut() + .expect("Missing burnchain epochs in config"); + epochs.truncate_after(StacksEpochId::Epoch33); + epochs + .get_mut(StacksEpochId::Epoch33) + .expect("Missing epoch 3.3 in config") + .end_height = STACKS_EPOCH_MAX; + } let sender_sk = Secp256k1PrivateKey::random(); let sender_signer_sk = Secp256k1PrivateKey::random(); let sender_signer_addr = tests::to_addr(&sender_signer_sk); @@ -15456,7 +15592,7 @@ fn check_block_time_keyword() { naka_conf.burnchain.chain_id, contract_name, contract, - Some(ClarityVersion::latest()), + Some(ClarityVersion::Clarity4), ); sender_nonce += 1; submit_tx(&http_origin, &contract_tx); @@ -15601,6 +15737,496 @@ fn check_block_time_keyword() { run_loop_thread.join().unwrap(); } +#[test] +#[ignore] +/// Verify `originator` mode and NFT `maybe-sent` post-conditions are +/// rejected before Epoch 3.4 and accepted in Epoch 3.4. In epoch 3.4 +/// ensure that the `originator` mode and `maybe-sent` post-conditions are +/// working as expected. +fn check_sip040_post_conditions() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let mut signers = TestSigners::default(); + let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None); + let http_origin = format!("http://{}", &naka_conf.node.rpc_bind); + naka_conf.burnchain.chain_id = CHAIN_ID_TESTNET + 1; + let sender_sk = Secp256k1PrivateKey::random(); + let sender_addr = tests::to_addr(&sender_sk); + let sender_signer_sk = Secp256k1PrivateKey::random(); + let sender_signer_addr = tests::to_addr(&sender_signer_sk); + let deploy_fee = 3000; + let call_fee = 400; + naka_conf.add_initial_balance( + PrincipalData::from(sender_addr.clone()).to_string(), + deploy_fee + call_fee * 20, + ); + naka_conf.add_initial_balance( + PrincipalData::from(sender_signer_addr.clone()).to_string(), + 100000, + ); + + let stacker_sk = setup_stacker(&mut naka_conf); + + test_observer::spawn(); + test_observer::register_any(&mut naka_conf); + + let mut btcd_controller = BitcoinCoreController::from_stx_config(&naka_conf); + btcd_controller + .start_bitcoind() + .expect("Failed starting bitcoind"); + let mut btc_regtest_controller = BitcoinRegtestController::new(naka_conf.clone(), None); + btc_regtest_controller.bootstrap_chain(201); + + let mut run_loop = boot_nakamoto::BootRunLoop::new(naka_conf.clone()).unwrap(); + let run_loop_stopper = run_loop.get_termination_switch(); + let Counters { + blocks_processed, .. + } = run_loop.counters(); + let counters = run_loop.counters(); + + let coord_channel = run_loop.coordinator_channels(); + + let run_loop_thread = thread::Builder::new() + .name("run_loop".into()) + .spawn(move || run_loop.start(None, 0)) + .unwrap(); + wait_for_runloop(&blocks_processed); + + boot_to_epoch_3( + &naka_conf, + &blocks_processed, + &[stacker_sk.clone()], + &[sender_signer_sk], + &mut Some(&mut signers), + &mut btc_regtest_controller, + ); + + info!("Bootstrapped to Epoch-3.0 boundary, starting nakamoto miner"); + + info!("Nakamoto miner started..."); + blind_signer(&naka_conf, &signers, &counters); + wait_for_first_naka_block_commit(60, &counters.naka_submitted_commits); + + let epoch33_start = + naka_conf.burnchain.epochs.as_ref().unwrap()[StacksEpochId::Epoch33].start_height; + let epoch34_start = + naka_conf.burnchain.epochs.as_ref().unwrap()[StacksEpochId::Epoch34].start_height; + // Boot through epoch 3.3, ensuring we don't miss the window for testing pre-3.4 behavior + loop { + let burn_height = get_chain_info_result(&naka_conf).unwrap().burn_block_height; + if burn_height >= epoch33_start && burn_height < epoch34_start { + break; + } + assert!( + burn_height < epoch34_start, + "Missed epoch 3.3 window at burn height {burn_height}" + ); + next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 60, &coord_channel) + .unwrap(); + } + + let mut sender_nonce = 0; + let contract_name = "pc-gate"; + let deploy_tx = make_contract_publish( + &sender_sk, + sender_nonce, + deploy_fee, + naka_conf.burnchain.chain_id, + contract_name, + r#" +(define-data-var postcond-flag bool false) +(define-non-fungible-token asset uint) +(define-public (ping) (ok true)) +(define-public (set-flag) + (begin + (var-set postcond-flag true) + (ok true) + ) +) +(define-read-only (get-flag) (var-get postcond-flag)) +(define-public (mint (id uint)) + (begin + (try! (nft-mint? asset id tx-sender)) + (ok true) + ) +) +(define-public (send (id uint) (recipient principal)) + (begin + (try! (nft-transfer? asset id tx-sender recipient)) + (ok true) + ) +) +(define-public (mint-and-send-as-contract (id uint) (recipient principal)) + (begin + (try! (nft-mint? asset id current-contract)) + (try! (nft-transfer? asset id current-contract recipient)) + (ok true) + ) +) +(define-read-only (get-owner (id uint)) (nft-get-owner? asset id)) +"#, + ); + sender_nonce += 1; + let deploy_txid = submit_tx(&http_origin, &deploy_tx); + info!("Submitted deploy txid: {deploy_txid}"); + wait_for(60, || { + let cur_sender_nonce = get_account(&http_origin, &sender_addr).nonce; + Ok(cur_sender_nonce == sender_nonce) + }) + .expect("Timed out waiting for contract deployment"); + + // A transaction with `originator` mode should be rejected before epoch 3.4 + let originator_tx = make_contract_call_with_post_conditions( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "ping", + &[], + TransactionPostConditionMode::Originator, + vec![], + ); + let originator_err = submit_tx_fallible(&http_origin, &originator_tx).unwrap_err(); + assert!(originator_err.contains("Originator post-condition mode")); + + // A transaction with a `maybe-sent` post-condition should be rejected before epoch 3.4 + let maybe_sent_post_condition = TransactionPostCondition::Nonfungible( + PostConditionPrincipal::Origin, + AssetInfo { + contract_address: sender_addr.clone(), + contract_name: ContractName::from(contract_name), + asset_name: ClarityName::from("asset"), + }, + Value::UInt(1), + NonfungibleConditionCode::MaybeSent, + ); + let maybe_sent_tx = make_contract_call_with_post_conditions( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "ping", + &[], + TransactionPostConditionMode::Deny, + vec![maybe_sent_post_condition.clone()], + ); + let maybe_sent_err = submit_tx_fallible(&http_origin, &maybe_sent_tx).unwrap_err(); + assert!(maybe_sent_err.contains("MaybeSent post-condition")); + assert_eq!(get_account(&http_origin, &sender_addr).nonce, sender_nonce); + + // Boot to 3.4 + loop { + let burn_height = get_chain_info_result(&naka_conf).unwrap().burn_block_height; + if burn_height >= epoch34_start { + break; + } + next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 60, &coord_channel) + .unwrap(); + } + + // Simple `originator` mode transaction should now be accepted + test_observer::clear(); + let originator_txid = submit_tx(&http_origin, &originator_tx); + wait_for(60, || { + let cur_sender_nonce = get_account(&http_origin, &sender_addr).nonce; + Ok(cur_sender_nonce == sender_nonce + 1) + }) + .expect("Timed out waiting for originator tx"); + assert_eq!( + get_tx_result_by_id(&originator_txid), + Some(Value::okay_true()) + ); + sender_nonce += 1; + + // A transaction with a `maybe-sent` post-condition should now be accepted + test_observer::clear(); + let maybe_sent_tx = make_contract_call_with_post_conditions( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "ping", + &[], + TransactionPostConditionMode::Deny, + vec![maybe_sent_post_condition.clone()], + ); + let maybe_sent_txid = submit_tx(&http_origin, &maybe_sent_tx); + wait_for(60, || { + let cur_sender_nonce = get_account(&http_origin, &sender_addr).nonce; + Ok(cur_sender_nonce == sender_nonce + 1) + }) + .expect("Timed out waiting for maybe-sent tx"); + assert_eq!( + get_tx_status_by_id(&maybe_sent_txid).as_deref(), + Some("success") + ); + assert_eq!( + get_tx_result_by_id(&maybe_sent_txid), + Some(Value::okay_true()) + ); + sender_nonce += 1; + + // Mint some NFTs to our sender + let recipient = tests::to_addr(&Secp256k1PrivateKey::random()); + for id in [1u128, 2u128] { + test_observer::clear(); + let mint_tx = make_contract_call( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "mint", + &[Value::UInt(id)], + ); + let mint_txid = submit_tx(&http_origin, &mint_tx); + wait_for(60, || { + let cur_sender_nonce = get_account(&http_origin, &sender_addr).nonce; + Ok(cur_sender_nonce == sender_nonce + 1) + }) + .expect("Timed out waiting for mint tx"); + assert_eq!(get_tx_status_by_id(&mint_txid).as_deref(), Some("success")); + assert_eq!(get_tx_result_by_id(&mint_txid), Some(Value::okay_true())); + sender_nonce += 1; + } + + let owner_1_before = call_read_only( + &naka_conf, + &sender_addr, + contract_name, + "get-owner", + vec![&Value::UInt(1)], + ) + .result() + .unwrap() + .expect_optional() + .unwrap() + .unwrap(); + assert_eq!(owner_1_before, Value::Principal(sender_addr.clone().into())); + + let owner_2_before = call_read_only( + &naka_conf, + &sender_addr, + contract_name, + "get-owner", + vec![&Value::UInt(2)], + ) + .result() + .unwrap() + .expect_optional() + .unwrap() + .unwrap(); + assert_eq!(owner_2_before, Value::Principal(sender_addr.clone().into())); + + // A transfer that satisfies the `maybe-sent` post-condition should succeed + test_observer::clear(); + let maybe_sent_good_tx = make_contract_call_with_post_conditions( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "send", + &[Value::UInt(1), Value::Principal(recipient.clone().into())], + TransactionPostConditionMode::Deny, + vec![maybe_sent_post_condition.clone()], + ); + let maybe_sent_good_txid = submit_tx(&http_origin, &maybe_sent_good_tx); + wait_for(60, || { + let cur_sender_nonce = get_account(&http_origin, &sender_addr).nonce; + Ok(cur_sender_nonce == sender_nonce + 1) + }) + .expect("Timed out waiting for maybe-sent success transfer"); + assert_eq!( + get_tx_status_by_id(&maybe_sent_good_txid).as_deref(), + Some("success") + ); + assert_eq!( + get_tx_result_by_id(&maybe_sent_good_txid), + Some(Value::okay_true()) + ); + sender_nonce += 1; + + // The owner of token 1 should now be the recipient + let owner_1_after_good = call_read_only( + &naka_conf, + &sender_addr, + contract_name, + "get-owner", + vec![&Value::UInt(1)], + ) + .result() + .unwrap() + .expect_optional() + .unwrap() + .unwrap(); + assert_eq!( + owner_1_after_good, + Value::Principal(recipient.clone().into()) + ); + + // A transfer that does not satisfy the `maybe-sent` post-condition + // (because it moves a different token) should fail + test_observer::clear(); + let maybe_sent_mismatch_tx = make_contract_call_with_post_conditions( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "send", + &[Value::UInt(2), Value::Principal(recipient.clone().into())], + TransactionPostConditionMode::Deny, + vec![maybe_sent_post_condition], + ); + let maybe_sent_mismatch_txid = submit_tx(&http_origin, &maybe_sent_mismatch_tx); + wait_for(60, || { + let cur_sender_nonce = get_account(&http_origin, &sender_addr).nonce; + Ok(cur_sender_nonce == sender_nonce + 1) + }) + .expect("Timed out waiting for maybe-sent mismatch transfer"); + assert_eq!( + get_tx_status_by_id(&maybe_sent_mismatch_txid).as_deref(), + Some("abort_by_post_condition") + ); + sender_nonce += 1; + + // The owner of token 2 should still be the sender since the transfer + // should have been aborted by the post-condition + let owner_2_after_mismatch = call_read_only( + &naka_conf, + &sender_addr, + contract_name, + "get-owner", + vec![&Value::UInt(2)], + ) + .result() + .unwrap() + .expect_optional() + .unwrap() + .unwrap(); + assert_eq!( + owner_2_after_mismatch, + Value::Principal(sender_addr.clone().into()) + ); + + // A transaction that fails an `originator` post-condition should have its + // side effects rolled back + test_observer::clear(); + let originator_fail_tx = make_contract_call_with_post_conditions( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "set-flag", + &[], + TransactionPostConditionMode::Originator, + vec![TransactionPostCondition::STX( + PostConditionPrincipal::Origin, + FungibleConditionCode::SentGt, + 0, + )], + ); + let originator_fail_txid = submit_tx(&http_origin, &originator_fail_tx); + wait_for(60, || { + let cur_sender_nonce = get_account(&http_origin, &sender_addr).nonce; + Ok(cur_sender_nonce == sender_nonce + 1) + }) + .expect("Timed out waiting for originator failure tx"); + assert_eq!( + get_tx_status_by_id(&originator_fail_txid).as_deref(), + Some("abort_by_post_condition") + ); + sender_nonce += 1; + + let flag_after_originator_fail = + call_read_only(&naka_conf, &sender_addr, contract_name, "get-flag", vec![]) + .result() + .unwrap() + .expect_bool() + .unwrap(); + assert!( + !flag_after_originator_fail, + "set-flag side effect should roll back on post-condition abort" + ); + + // `mint-and-send-as-contract` should fail in deny mode with no post-conditions. + let mint_and_send_as_contract_tx = make_contract_call_with_post_conditions( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "mint-and-send-as-contract", + &[Value::UInt(3), Value::Principal(recipient.clone().into())], + TransactionPostConditionMode::Deny, + vec![], + ); + let mint_and_send_as_contract_txid = submit_tx(&http_origin, &mint_and_send_as_contract_tx); + wait_for(60, || { + let cur_sender_nonce = get_account(&http_origin, &sender_addr).nonce; + Ok(cur_sender_nonce == sender_nonce + 1) + }) + .expect("Timed out waiting for mint-and-send-as-contract tx"); + assert_eq!( + get_tx_status_by_id(&mint_and_send_as_contract_txid).as_deref(), + Some("abort_by_post_condition") + ); + sender_nonce += 1; + + // mint-and-send-as-contract should succeed in originator mode with no post-conditions since + // no assets are sent from the originating account. + let mint_and_send_as_contract_originator_tx = make_contract_call_with_post_conditions( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "mint-and-send-as-contract", + &[Value::UInt(3), Value::Principal(recipient.clone().into())], + TransactionPostConditionMode::Originator, + vec![], + ); + let mint_and_send_as_contract_originator_txid = + submit_tx(&http_origin, &mint_and_send_as_contract_originator_tx); + wait_for(60, || { + let cur_sender_nonce = get_account(&http_origin, &sender_addr).nonce; + Ok(cur_sender_nonce == sender_nonce + 1) + }) + .expect("Timed out waiting for mint-and-send-as-contract originator tx"); + assert_eq!( + get_tx_status_by_id(&mint_and_send_as_contract_originator_txid).as_deref(), + Some("success") + ); + assert_eq!( + get_tx_result_by_id(&mint_and_send_as_contract_originator_txid), + Some(Value::okay_true()) + ); + + coord_channel + .lock() + .expect("Mutex poisoned") + .stop_chains_coordinator(); + run_loop_stopper.store(false, Ordering::SeqCst); + + run_loop_thread.join().unwrap(); +} + #[test] #[ignore] /// Verify the `with-stacking` allowances work as expected when delegating STX. @@ -18694,7 +19320,7 @@ fn tenure_extend_no_commits() { test_observer::clear(); // Skip block commits so that for the next block, there is no new commit - counters.naka_skip_commit_op.set(true); + counters.skip_commit_op.set(true); // Mine an empty Bitcoin block (no commits) info!("1. Mining an empty Bitcoin block, even though the miner had submitted a valid commit"); diff --git a/stacks-node/src/tests/neon_integrations.rs b/stacks-node/src/tests/neon_integrations.rs index 621cbf01000..85f722c4eaa 100644 --- a/stacks-node/src/tests/neon_integrations.rs +++ b/stacks-node/src/tests/neon_integrations.rs @@ -4297,41 +4297,41 @@ fn cost_voting_integration() { &[Value::UInt(1)], ); + test_observer::clear(); submit_tx(&http_origin, &vote_tx); submit_tx(&http_origin, &call_le_tx); - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - - // clear and mine another burnchain block, so that the new winner is seen by the observer - // (the observer is logically "one block behind" the miner - test_observer::clear(); - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + // Mine blocks until both txs are confirmed (nonces 3 and 4) + wait_for(120, || { + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + let res = get_account(&http_origin, &spender_princ); + Ok(res.nonce >= 5) + }) + .expect("vote and execute txs should have been mined"); - let mut blocks = test_observer::get_blocks(); - // should have produced 1 new block - assert_eq!(blocks.len(), 1); - let block = blocks.pop().unwrap(); - let transactions = block.get("transactions").unwrap().as_array().unwrap(); - eprintln!("{}", transactions.len()); + let blocks = test_observer::get_blocks(); let mut tested = false; let mut exec_cost = ExecutionCost::ZERO; - for tx in transactions.iter() { - let raw_tx = tx.get("raw_tx").unwrap().as_str().unwrap(); - if raw_tx == "0x00" { - continue; - } - let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); - let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); - if let TransactionPayload::ContractCall(contract_call) = parsed.payload { - eprintln!("{}", contract_call.function_name.as_str()); - if contract_call.function_name.as_str() == "execute-2" { - exec_cost = - serde_json::from_value(tx.get("execution_cost").cloned().unwrap()).unwrap(); - } else if contract_call.function_name.as_str() == "propose-vote-confirm" { - let raw_result = tx.get("raw_result").unwrap().as_str().unwrap(); - let parsed = Value::try_deserialize_hex_untyped(&raw_result[2..]).unwrap(); - assert_eq!(parsed.to_string(), "(ok u0)"); - tested = true; + for block in blocks.iter() { + let transactions = block.get("transactions").unwrap().as_array().unwrap(); + for tx in transactions.iter() { + let raw_tx = tx.get("raw_tx").unwrap().as_str().unwrap(); + if raw_tx == "0x00" { + continue; + } + let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); + let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); + if let TransactionPayload::ContractCall(contract_call) = parsed.payload { + eprintln!("{}", contract_call.function_name.as_str()); + if contract_call.function_name.as_str() == "execute-2" { + exec_cost = + serde_json::from_value(tx.get("execution_cost").cloned().unwrap()).unwrap(); + } else if contract_call.function_name.as_str() == "propose-vote-confirm" { + let raw_result = tx.get("raw_result").unwrap().as_str().unwrap(); + let parsed = Value::try_deserialize_hex_untyped(&raw_result[2..]).unwrap(); + assert_eq!(parsed.to_string(), "(ok u0)"); + tested = true; + } } } } @@ -4349,36 +4349,36 @@ fn cost_voting_integration() { &[Value::UInt(0)], ); + test_observer::clear(); submit_tx(&http_origin, &confirm_proposal); - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - - // clear and mine another burnchain block, so that the new winner is seen by the observer - // (the observer is logically "one block behind" the miner - test_observer::clear(); - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + // Mine blocks until early confirm-miners is confirmed (nonce 5) + wait_for(120, || { + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + let res = get_account(&http_origin, &spender_princ); + Ok(res.nonce >= 6) + }) + .expect("early confirm-miners tx should have been mined"); - let mut blocks = test_observer::get_blocks(); - // should have produced 1 new block - assert_eq!(blocks.len(), 1); - let block = blocks.pop().unwrap(); - let transactions = block.get("transactions").unwrap().as_array().unwrap(); - eprintln!("{}", transactions.len()); + let blocks = test_observer::get_blocks(); let mut tested = false; - for tx in transactions.iter() { - let raw_tx = tx.get("raw_tx").unwrap().as_str().unwrap(); - if raw_tx == "0x00" { - continue; - } - let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); - let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); - if let TransactionPayload::ContractCall(contract_call) = parsed.payload { - eprintln!("{}", contract_call.function_name.as_str()); - if contract_call.function_name.as_str() == "confirm-miners" { - let raw_result = tx.get("raw_result").unwrap().as_str().unwrap(); - let parsed = Value::try_deserialize_hex_untyped(&raw_result[2..]).unwrap(); - assert_eq!(parsed.to_string(), "(err 13)"); - tested = true; + for block in blocks.iter() { + let transactions = block.get("transactions").unwrap().as_array().unwrap(); + for tx in transactions.iter() { + let raw_tx = tx.get("raw_tx").unwrap().as_str().unwrap(); + if raw_tx == "0x00" { + continue; + } + let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); + let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); + if let TransactionPayload::ContractCall(contract_call) = parsed.payload { + eprintln!("{}", contract_call.function_name.as_str()); + if contract_call.function_name.as_str() == "confirm-miners" { + let raw_result = tx.get("raw_result").unwrap().as_str().unwrap(); + let parsed = Value::try_deserialize_hex_untyped(&raw_result[2..]).unwrap(); + assert_eq!(parsed.to_string(), "(err 13)"); + tested = true; + } } } } @@ -4400,35 +4400,36 @@ fn cost_voting_integration() { &[Value::UInt(0)], ); + test_observer::clear(); submit_tx(&http_origin, &confirm_proposal); - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - // clear and mine another burnchain block, so that the new winner is seen by the observer - // (the observer is logically "one block behind" the miner - test_observer::clear(); - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + // Mine blocks until confirm-miners after maturation is confirmed (nonce 6) + wait_for(120, || { + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + let res = get_account(&http_origin, &spender_princ); + Ok(res.nonce >= 7) + }) + .expect("confirm-miners tx should have been mined"); - let mut blocks = test_observer::get_blocks(); - // should have produced 1 new block - assert_eq!(blocks.len(), 1); - let block = blocks.pop().unwrap(); - let transactions = block.get("transactions").unwrap().as_array().unwrap(); - eprintln!("{}", transactions.len()); + let blocks = test_observer::get_blocks(); let mut tested = false; - for tx in transactions.iter() { - let raw_tx = tx.get("raw_tx").unwrap().as_str().unwrap(); - if raw_tx == "0x00" { - continue; - } - let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); - let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); - if let TransactionPayload::ContractCall(contract_call) = parsed.payload { - eprintln!("{}", contract_call.function_name.as_str()); - if contract_call.function_name.as_str() == "confirm-miners" { - let raw_result = tx.get("raw_result").unwrap().as_str().unwrap(); - let parsed = Value::try_deserialize_hex_untyped(&raw_result[2..]).unwrap(); - assert_eq!(parsed.to_string(), "(ok true)"); - tested = true; + for block in blocks.iter() { + let transactions = block.get("transactions").unwrap().as_array().unwrap(); + for tx in transactions.iter() { + let raw_tx = tx.get("raw_tx").unwrap().as_str().unwrap(); + if raw_tx == "0x00" { + continue; + } + let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); + let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); + if let TransactionPayload::ContractCall(contract_call) = parsed.payload { + eprintln!("{}", contract_call.function_name.as_str()); + if contract_call.function_name.as_str() == "confirm-miners" { + let raw_result = tx.get("raw_result").unwrap().as_str().unwrap(); + let parsed = Value::try_deserialize_hex_untyped(&raw_result[2..]).unwrap(); + assert_eq!(parsed.to_string(), "(ok true)"); + tested = true; + } } } } @@ -4445,35 +4446,36 @@ fn cost_voting_integration() { &[Value::UInt(1)], ); - submit_tx(&http_origin, &call_le_tx); - - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); - // clear and mine another burnchain block, so that the new winner is seen by the observer - // (the observer is logically "one block behind" the miner test_observer::clear(); - next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + submit_tx(&http_origin, &call_le_tx); - let mut blocks = test_observer::get_blocks(); - // should have produced 1 new block - assert_eq!(blocks.len(), 1); - let block = blocks.pop().unwrap(); - let transactions = block.get("transactions").unwrap().as_array().unwrap(); + // Mine blocks until execute-2 with new cost is confirmed (nonce 7) + wait_for(120, || { + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + let res = get_account(&http_origin, &spender_princ); + Ok(res.nonce >= 8) + }) + .expect("execute-2 tx should have been mined"); + let blocks = test_observer::get_blocks(); let mut tested = false; let mut new_exec_cost = ExecutionCost::max_value(); - for tx in transactions.iter() { - let raw_tx = tx.get("raw_tx").unwrap().as_str().unwrap(); - if raw_tx == "0x00" { - continue; - } - let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); - let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); - if let TransactionPayload::ContractCall(contract_call) = parsed.payload { - eprintln!("{}", contract_call.function_name.as_str()); - if contract_call.function_name.as_str() == "execute-2" { - new_exec_cost = - serde_json::from_value(tx.get("execution_cost").cloned().unwrap()).unwrap(); - tested = true; + for block in blocks.iter() { + let transactions = block.get("transactions").unwrap().as_array().unwrap(); + for tx in transactions.iter() { + let raw_tx = tx.get("raw_tx").unwrap().as_str().unwrap(); + if raw_tx == "0x00" { + continue; + } + let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); + let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); + if let TransactionPayload::ContractCall(contract_call) = parsed.payload { + eprintln!("{}", contract_call.function_name.as_str()); + if contract_call.function_name.as_str() == "execute-2" { + new_exec_cost = + serde_json::from_value(tx.get("execution_cost").cloned().unwrap()).unwrap(); + tested = true; + } } } } @@ -7343,22 +7345,26 @@ fn fuzzed_median_fee_rate_estimation_test(window_size: u64, expected_final_value // Check that: // 1) The cost is always the same. - // 2) Fee rate grows monotonically. + // 2) Fee rate trends upward overall. With 2 blocks mined per transaction the + // estimator window contains a mix of transaction-bearing and empty blocks. + // Empty blocks contribute fee_rate=1.0 (the minimum), which can cause + // intermediate dips in the median — so we verify the overall trend rather + // than strict monotonicity at every step. for i in 1..response_estimated_costs.len() { let curr_cost = response_estimated_costs[i]; let last_cost = response_estimated_costs[i - 1]; assert_eq!(curr_cost, last_cost); - - let curr_rate = response_top_fee_rates[i]; - let last_rate = response_top_fee_rates[i - 1]; - assert!(curr_rate >= last_rate); } - // Check the final value is near input parameter. - assert!(is_close_f64( - *response_top_fee_rates.last().unwrap(), - expected_final_value - )); + let first_rate = *response_top_fee_rates.first().unwrap(); + let last_rate = *response_top_fee_rates.last().unwrap(); + assert!( + last_rate > first_rate, + "Fee rate should trend upward: first={first_rate}, last={last_rate}" + ); + + // Check the final value is near the expected value. + assert!(is_close_f64(last_rate, expected_final_value)); channel.stop_chains_coordinator(); } @@ -9471,7 +9477,7 @@ fn mock_miner_replay() { return; } - let timeout = Some(Duration::from_secs(30)); + let timeout = Some(Duration::from_secs(120)); // Had to add this so that mock miner makes an attempt on EVERY block let block_gap = Duration::from_secs(1); diff --git a/stacks-node/src/tests/signer/commands/block_commit.rs b/stacks-node/src/tests/signer/commands/block_commit.rs index c5ceccf99ef..50c5999118d 100644 --- a/stacks-node/src/tests/signer/commands/block_commit.rs +++ b/stacks-node/src/tests/signer/commands/block_commit.rs @@ -22,7 +22,7 @@ impl Command for MinerSubmitNakaBlockCommit let is_miner_paused = self .ctx .get_counters_for_miner(self.miner_index) - .naka_skip_commit_op + .skip_commit_op .get(); info!( @@ -49,13 +49,13 @@ impl Command for MinerSubmitNakaBlockCommit .miners .lock() .unwrap() - .submit_commit_miner_1(&sortdb), + .ensure_commit_miner_1(&sortdb), 2 => self .ctx .miners .lock() .unwrap() - .submit_commit_miner_2(&sortdb), + .ensure_commit_miner_2(&sortdb), _ => panic!( "Invalid miner index: {}. Expected 1 or 2.", self.miner_index @@ -65,7 +65,7 @@ impl Command for MinerSubmitNakaBlockCommit assert!(self .ctx .get_counters_for_miner(self.miner_index) - .naka_skip_commit_op + .skip_commit_op .get()); } diff --git a/stacks-node/src/tests/signer/commands/commit_ops.rs b/stacks-node/src/tests/signer/commands/commit_ops.rs index 4c63a6cfb2a..a0070033f27 100644 --- a/stacks-node/src/tests/signer/commands/commit_ops.rs +++ b/stacks-node/src/tests/signer/commands/commit_ops.rs @@ -38,7 +38,7 @@ impl Command for ChainMinerCommitOp { let current_state = self .ctx .get_counters_for_miner(self.miner_index) - .naka_skip_commit_op + .skip_commit_op .get(); let should_apply = current_state != self.skip; @@ -58,7 +58,7 @@ impl Command for ChainMinerCommitOp { ); self.ctx .get_counters_for_miner(self.miner_index) - .naka_skip_commit_op + .skip_commit_op .set(self.skip); } diff --git a/stacks-node/src/tests/signer/mod.rs b/stacks-node/src/tests/signer/mod.rs index f8c39e6149f..76bc67d3bc3 100644 --- a/stacks-node/src/tests/signer/mod.rs +++ b/stacks-node/src/tests/signer/mod.rs @@ -83,7 +83,9 @@ use crate::tests::neon_integrations::{ get_chain_info, next_block_and_wait, run_until_burnchain_height, test_observer, wait_for_runloop, }; -use crate::tests::signer::v0::wait_for_state_machine_update_by_miner_tenure_id; +use crate::tests::signer::v0::{ + wait_for_state_machine_update, wait_for_state_machine_update_by_miner_tenure_id, +}; use crate::tests::to_addr; use crate::BitcoinRegtestController; @@ -566,6 +568,27 @@ impl SignerTest { ); } + /// Wait for >70% of signers to update their global state to the + /// current burn block tip. Call this after mining a bitcoin block + /// when you need signers to have the correct burn block view before + /// proceeding (e.g., before checking for specific block proposals). + /// Mining is skipped during the wait to prevent the miner from + /// proposing a block before signers have the correct view. + pub fn wait_for_signer_state_update(&self) { + let was_skipping = TEST_MINE_SKIP.get(); + TEST_MINE_SKIP.set(true); + let peer_info = get_chain_info(&self.running_nodes.conf); + wait_for_state_machine_update( + 30, + &peer_info.pox_consensus, + peer_info.burn_block_height, + None, + &self.signer_addresses_versions_majority(), + ) + .expect("Signers failed to update to new burn block view"); + TEST_MINE_SKIP.set(was_skipping); + } + /// Fetch the local signer state machine for all the signers, /// waiting until every signer has processed the latest burn block. /// Then, check that every signer's state machine corresponds to the diff --git a/stacks-node/src/tests/signer/v0/capitulate_parent_tenure_view.rs b/stacks-node/src/tests/signer/v0/capitulate_parent_tenure_view.rs index 355784f6908..fb311bd6bd0 100644 --- a/stacks-node/src/tests/signer/v0/capitulate_parent_tenure_view.rs +++ b/stacks-node/src/tests/signer/v0/capitulate_parent_tenure_view.rs @@ -16,7 +16,6 @@ use std::env; use std::time::Duration; use clarity::vm::types::PrincipalData; -use stacks::codec::StacksMessageCodec; use stacks::core::test_util::make_stacks_transfer_serialized; use stacks::types::chainstate::{StacksAddress, StacksPublicKey}; use stacks::util::secp256k1::Secp256k1PrivateKey; @@ -134,12 +133,13 @@ fn deadlock_50_50_split_capitulates_to_node_tip() { .expect("Timed out waiting for N to be mined and processed"); // Ensure that the block was accepted globally so the stacks tip has advanced to N - let block_n = - wait_for_block_pushed_by_miner_key(30, info_before.stacks_tip_height + 1, &miner_pk) - .expect("Timed out waiting for block N to be mined"); + let _block_n = + wait_for_block_pushed_and_tip(30, info_before.stacks_tip_height + 1, &miner_pk, || { + signer_test.get_peer_info().stacks_tip + }) + .expect("Timed out waiting for block N to be mined"); let info_after = signer_test.get_peer_info(); - assert_eq!(info_after.stacks_tip, block_n.header.block_hash()); assert_eq!( info_after.stacks_tip_height, info_before.stacks_tip_height + 1 @@ -204,17 +204,9 @@ fn deadlock_50_50_split_capitulates_to_node_tip() { .collect(); let signer_addresses = signer_test.signer_addresses_versions(); wait_for(30, || { - let stackerdb_events = test_observer::get_stackerdb_chunks(); let mut found_updates_n: HashSet = HashSet::new(); let mut found_updates_n_1: HashSet = HashSet::new(); - for chunk in stackerdb_events - .into_iter() - .flat_map(|chunk| chunk.modified_slots) - { - let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - else { - continue; - }; + for (chunk, message) in get_stackerdb_signer_messages() { let SignerMessage::StateMachineUpdate(update) = message else { continue; }; @@ -268,16 +260,8 @@ fn deadlock_50_50_split_capitulates_to_node_tip() { ); std::thread::sleep(time_to_wait); wait_for(30, || { - let stackerdb_events = test_observer::get_stackerdb_chunks(); let mut found_updates_n: HashSet = HashSet::new(); - for chunk in stackerdb_events - .into_iter() - .flat_map(|chunk| chunk.modified_slots) - { - let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - else { - continue; - }; + for (chunk, message) in get_stackerdb_signer_messages() { let SignerMessage::StateMachineUpdate(update) = message else { continue; }; @@ -417,12 +401,13 @@ fn minority_signers_capitulate_to_supermajority_consensus() { .expect("Timed out waiting for N to be mined and processed"); // Ensure that the block was accepted globally so the stacks tip has advanced to N - let block_n = - wait_for_block_pushed_by_miner_key(30, info_before.stacks_tip_height + 1, &miner_pk) - .expect("Timed out waiting for block N to be mined"); + let _block_n = + wait_for_block_pushed_and_tip(30, info_before.stacks_tip_height + 1, &miner_pk, || { + signer_test.get_peer_info().stacks_tip + }) + .expect("Timed out waiting for block N to be mined"); let info_after = signer_test.get_peer_info(); - assert_eq!(info_after.stacks_tip, block_n.header.block_hash()); assert_eq!( info_after.stacks_tip_height, info_before.stacks_tip_height + 1 @@ -493,17 +478,9 @@ fn minority_signers_capitulate_to_supermajority_consensus() { .collect(); let signer_addresses = signer_test.signer_addresses_versions(); wait_for(30, || { - let stackerdb_events = test_observer::get_stackerdb_chunks(); let mut found_updates_n: HashSet = HashSet::new(); let mut found_updates_n_1: HashSet = HashSet::new(); - for chunk in stackerdb_events - .into_iter() - .flat_map(|chunk| chunk.modified_slots) - { - let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - else { - continue; - }; + for (chunk, message) in get_stackerdb_signer_messages() { let SignerMessage::StateMachineUpdate(update) = message else { continue; }; @@ -561,16 +538,8 @@ fn minority_signers_capitulate_to_supermajority_consensus() { ); std::thread::sleep(time_to_wait); wait_for(30, || { - let stackerdb_events = test_observer::get_stackerdb_chunks(); let mut found_updates_n_1: HashSet = HashSet::new(); - for chunk in stackerdb_events - .into_iter() - .flat_map(|chunk| chunk.modified_slots) - { - let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - else { - continue; - }; + for (chunk, message) in get_stackerdb_signer_messages() { let SignerMessage::StateMachineUpdate(update) = message else { continue; }; diff --git a/stacks-node/src/tests/signer/v0/late_block_proposal.rs b/stacks-node/src/tests/signer/v0/late_block_proposal.rs index 6462a1eda71..2ddabc1da7d 100644 --- a/stacks-node/src/tests/signer/v0/late_block_proposal.rs +++ b/stacks-node/src/tests/signer/v0/late_block_proposal.rs @@ -18,7 +18,6 @@ use std::time::Duration; use clarity::vm::types::PrincipalData; use libsigner::v0::messages::{BlockResponse, SignerMessage}; use pinny::tag; -use stacks::codec::StacksMessageCodec; use stacks::core::test_util::{make_stacks_transfer_serialized, to_addr}; use stacks::types::chainstate::{StacksAddress, StacksPublicKey}; use stacks::types::PublicKey; @@ -31,7 +30,9 @@ use tracing_subscriber::{fmt, EnvFilter}; use super::SignerTest; use crate::tests::nakamoto_integrations::wait_for; use crate::tests::neon_integrations::{get_chain_info, submit_tx, test_observer}; -use crate::tests::signer::v0::{wait_for_block_proposal, wait_for_block_pushed}; +use crate::tests::signer::v0::{ + get_stackerdb_signer_messages, wait_for_block_proposal, wait_for_block_pushed_and_tip, +}; #[tag(bitcoind)] #[test] @@ -107,22 +108,14 @@ fn signer_rejects_proposal_after_block_pushed() { wait_for_block_proposal(30, info_before.stacks_tip_height + 1, &miner_pk) .expect("Timed out waiting for block N+1 to be proposed"); let signer_signature_hash = block_n_proposal.block.header.signer_signature_hash(); - let _ = wait_for_block_pushed(30, &signer_signature_hash) - .expect("Failed to get BlockPushed for block N"); - info!("------------------------- Advance Chain to Include Block N -------------------------"); - // Shouldn't have to wait long for the chain to advance - wait_for(10, || { - let info_after = get_chain_info(&signer_test.running_nodes.conf); - Ok(info_after.stacks_tip_height >= info_before.stacks_tip_height + 1) + let _ = wait_for_block_pushed_and_tip(30, info_before.stacks_tip_height + 1, &miner_pk, || { + get_chain_info(&signer_test.running_nodes.conf).stacks_tip }) - .expect("Chain did not advance to block N+1"); + .expect("Failed to get BlockPushed for block N"); info!("------------------------- Verify Signer 1 did NOT respond to the Block Proposal -------------------------"); - let chunks = test_observer::get_stackerdb_chunks(); - for chunk in chunks.into_iter().flat_map(|chunk| chunk.modified_slots) { - let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) else { - continue; - }; + let messages = get_stackerdb_signer_messages(); + for (_chunk, message) in messages { match message { SignerMessage::BlockResponse(BlockResponse::Rejected(rejected)) => { if rejected.signer_signature_hash == signer_signature_hash { @@ -165,14 +158,8 @@ fn signer_rejects_proposal_after_block_pushed() { "------------------------- Verify Signer 1 Rejected the Proposal -------------------------" ); wait_for(30, || { - let chunks: Vec<_> = test_observer::get_stackerdb_chunks() - .into_iter() - .flat_map(|chunk| chunk.modified_slots) - .collect(); - for chunk in chunks { - let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - .expect("Failed to deserialize SignerMessage"); - + let messages = get_stackerdb_signer_messages(); + for (_chunk, message) in messages { let SignerMessage::BlockResponse(BlockResponse::Rejected(rejected)) = message else { continue; }; diff --git a/stacks-node/src/tests/signer/v0/mod.rs b/stacks-node/src/tests/signer/v0/mod.rs index 4f6b8c3f38d..c6b255d5602 100644 --- a/stacks-node/src/tests/signer/v0/mod.rs +++ b/stacks-node/src/tests/signer/v0/mod.rs @@ -40,7 +40,7 @@ use stacks::chainstate::burn::ConsensusHash; use stacks::chainstate::coordinator::comm::CoordinatorChannels; use stacks::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader, NakamotoChainState}; use stacks::chainstate::stacks::address::{PoxAddress, StacksAddressExtensions}; -use stacks::chainstate::stacks::boot::MINERS_NAME; +use stacks::chainstate::stacks::boot::{MINERS_NAME, SIGNERS_NAME}; use stacks::chainstate::stacks::db::{StacksChainState, StacksHeaderInfo}; use stacks::chainstate::stacks::miner::{TransactionEvent, TransactionSuccessEvent}; use stacks::chainstate::stacks::{StacksTransaction, TenureChangeCause, TransactionPayload}; @@ -144,24 +144,40 @@ impl SignerTest { // Make sure the signer set is calculated before continuing or signers may not // recognize that they are registered signers in the subsequent burn block event let reward_cycle = self.get_current_reward_cycle() + 1; - let mut last_probe = Instant::now(); - wait_for(120, || { + let next_cycle_start = self + .running_nodes + .btc_regtest_controller + .get_burnchain() + .nakamoto_first_block_of_cycle(reward_cycle); + wait_for(240, || { match self.stacks_client.get_reward_set_signers(reward_cycle).unwrap_or_default() { Some(reward_set) => { debug!("Signer set: {reward_set:?}"); Ok(true) } None => { - // If we've been waiting ~30s since the last probe, maybe the last block failed - // so we should try to mine another block - if last_probe.elapsed() >= Duration::from_secs(30) { + let burn_height = get_chain_info(&self.running_nodes.conf).burn_block_height; + if burn_height < next_cycle_start { + // Still in the prepare phase or before the cycle boundary. + // Mining another burn block is safe and may be needed for the + // anchor block to be determined. warn!( - "Timed out waiting for reward set calculation. Mining another block to try again." + "Reward set not yet available (burn_height={burn_height}, \ + cycle_start={next_cycle_start}). Mining another block." + ); + next_block_and_wait( + &self.running_nodes.btc_regtest_controller, + &self.running_nodes.counters.blocks_processed, + ); + } else { + // We've already crossed into the next cycle. Mining more burn + // blocks won't help — the anchor block should have been determined + // during the prepare phase. Just wait for the Stacks chain to + // catch up and process the existing blocks. + debug!( + "Reward set not yet available but already at burn_height={burn_height} \ + (>= cycle_start={next_cycle_start}). Waiting for Stacks chain to catch up." ); - self.running_nodes - .btc_regtest_controller - .build_next_block(1); - last_probe = Instant::now(); } Ok(false) } @@ -748,13 +764,25 @@ impl MultipleMinerTest { .clone() } - /// Boot node 1 to epoch 3.0 and wait for node 2 to catch up. + /// Boot both miners to epoch 3.0 and wait for them to sync. pub fn boot_to_epoch_3(&mut self) { info!( "------------------------- Booting Both Miners to Epoch 3.0 -------------------------" ); + // Prevent miner 2 from submitting block-commits during the boot + // phase. If miner 2 wins sortitions before its VRF key is properly + // registered it produces blocks with invalid VRF proofs, which can + // stall the chain and prevent the PoX anchor block from being + // determined. Save and restore the previous state so we don't + // clobber any test-level skip that was set before boot. + let prev_skip = self.rl2_counters.skip_commit_op.get(); + self.rl2_counters.skip_commit_op.set(true); + self.signer_test.boot_to_epoch_3(); + + self.rl2_counters.skip_commit_op.set(prev_skip); + // Use a longer timeout for the miners to advance to epoch 3.0 and so that CI runners don't timeout. self.wait_for_chains(600); @@ -1007,45 +1035,56 @@ impl MultipleMinerTest { } /// Ensures that miner 2 submits a commit pointing to the current view reported by the stacks node as expected - pub fn submit_commit_miner_2(&mut self, sortdb: &SortitionDB) { - if !self.rl2_counters.naka_skip_commit_op.get() { - warn!("Miner 2's commit ops were not paused. This may result in no commit being submitted."); - } + /// Ensures that miner 2 has submitted a commit pointing to the current + /// view reported by the stacks node. Temporarily unpauses commits if + /// needed, waits until the commit counters reflect the current burn + /// height and stacks tip, then restores the previous pause state. + pub fn ensure_commit_miner_2(&mut self, sortdb: &SortitionDB) { let burn_height = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()) .unwrap() .block_height; - let stacks_height_before = self.get_peer_stacks_tip_height(); - let rl2_commits_before = self - .rl2_counters - .naka_submitted_commits - .load(Ordering::SeqCst); - - info!("Unpausing commits from RL2"); - self.rl2_counters.naka_skip_commit_op.set(false); + let was_paused = self.rl2_counters.skip_commit_op.get(); + let commits_before = if was_paused { + info!("Unpausing commits from RL2"); + Some( + self.rl2_counters + .naka_submitted_commits + .load(Ordering::SeqCst), + ) + } else { + None + }; + self.unpause_commits_miner_2(); - info!("Waiting for commits from RL2"); + info!("Waiting for RL2 commit at burn_height={burn_height}, stacks_height={stacks_height_before}"); wait_for(30, || { - Ok(self + let height_ok = self .rl2_counters - .naka_submitted_commits + .naka_submitted_commit_last_burn_height .load(Ordering::SeqCst) - > rl2_commits_before - && self - .rl2_counters - .naka_submitted_commit_last_burn_height - .load(Ordering::SeqCst) - >= burn_height + >= burn_height && self .rl2_counters .naka_submitted_commit_last_stacks_tip .load(Ordering::SeqCst) - >= stacks_height_before) + >= stacks_height_before; + let commit_incremented = commits_before + .map(|before| { + self.rl2_counters + .naka_submitted_commits + .load(Ordering::SeqCst) + > before + }) + .unwrap_or(true); + Ok(height_ok && commit_incremented) }) .expect("Timed out waiting for miner 2 to submit a commit op"); - info!("Pausing commits from RL2"); - self.rl2_counters.naka_skip_commit_op.set(true); + if was_paused { + info!("Restoring paused state for RL2"); + self.pause_commits_miner_2(); + } } /// Pause miner 1's commits @@ -1053,76 +1092,87 @@ impl MultipleMinerTest { self.signer_test .running_nodes .counters - .naka_skip_commit_op + .skip_commit_op .set(true); } + /// Unpause miner 1's commits + pub fn unpause_commits_miner_1(&mut self) { + self.signer_test + .running_nodes + .counters + .skip_commit_op + .set(false); + } + /// Pause miner 2's commits pub fn pause_commits_miner_2(&mut self) { - self.rl2_counters.naka_skip_commit_op.set(true); + self.rl2_counters.skip_commit_op.set(true); } - /// Ensures that miner 1 submits a commit pointing to the current view reported by the stacks node as expected - pub fn submit_commit_miner_1(&mut self, sortdb: &SortitionDB) { - if !self - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .get() - { - warn!("Miner 1's commit ops were not paused. This may result in no commit being submitted."); - } + /// Unpause miner 2's commits + pub fn unpause_commits_miner_2(&mut self) { + self.rl2_counters.skip_commit_op.set(false); + } + + /// Ensures that miner 1 has submitted a commit pointing to the current + /// view reported by the stacks node. Temporarily unpauses commits if + /// needed, waits until the commit counters reflect the current burn + /// height and stacks tip, then restores the previous pause state. + pub fn ensure_commit_miner_1(&mut self, sortdb: &SortitionDB) { let burn_height = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()) .unwrap() .block_height; let stacks_height_before = self.get_peer_stacks_tip_height(); - let rl1_commits_before = self - .signer_test - .running_nodes - .counters - .naka_submitted_commits - .load(Ordering::SeqCst); - - info!("Unpausing commits from RL1"); - self.signer_test - .running_nodes - .counters - .naka_skip_commit_op - .set(false); + let was_paused = self.signer_test.running_nodes.counters.skip_commit_op.get(); + let commits_before = if was_paused { + info!("Unpausing commits from RL1"); + Some( + self.signer_test + .running_nodes + .counters + .naka_submitted_commits + .load(Ordering::SeqCst), + ) + } else { + None + }; + self.unpause_commits_miner_1(); - info!("Waiting for commits from RL1"); + info!("Waiting for RL1 commit at burn_height={burn_height}, stacks_height={stacks_height_before}"); wait_for(30, || { - Ok(self + let height_ok = self .signer_test .running_nodes .counters - .naka_submitted_commits + .naka_submitted_commit_last_burn_height .load(Ordering::SeqCst) - > rl1_commits_before - && self - .signer_test - .running_nodes - .counters - .naka_submitted_commit_last_burn_height - .load(Ordering::SeqCst) - >= burn_height + >= burn_height && self .signer_test .running_nodes .counters .naka_submitted_commit_last_stacks_tip .load(Ordering::SeqCst) - >= stacks_height_before) + >= stacks_height_before; + let commit_incremented = commits_before + .map(|before| { + self.signer_test + .running_nodes + .counters + .naka_submitted_commits + .load(Ordering::SeqCst) + > before + }) + .unwrap_or(true); + Ok(height_ok && commit_incremented) }) .expect("Timed out waiting for miner 1 to submit a commit op"); - info!("Pausing commits from RL1"); - self.signer_test - .running_nodes - .counters - .naka_skip_commit_op - .set(true); + if was_paused { + info!("Restoring paused state for RL1"); + self.pause_commits_miner_1(); + } } /// Shutdown the test harness @@ -1233,6 +1283,26 @@ pub fn wait_for_block_proposal_block( .and_then(|proposal| Ok(proposal.block)) } +/// Returns all successfully deserialized (StackerDBChunkData, SignerMessage) pairs +/// from the test_observer stackerdb chunks, filtered to only include chunks from +/// signer and miner contract IDs. +pub fn get_stackerdb_signer_messages() -> Vec<(StackerDBChunkData, SignerMessage)> { + test_observer::get_stackerdb_chunks() + .into_iter() + .filter(|event| { + event.contract_id.is_boot() + && (event.contract_id.name.starts_with(SIGNERS_NAME) + || event.contract_id.name.starts_with(MINERS_NAME)) + }) + .flat_map(|chunk| chunk.modified_slots) + .filter_map(|chunk| { + SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) + .ok() + .map(|msg| (chunk, msg)) + }) + .collect() +} + /// Waits for a block proposal to be observed in the test_observer stackerdb chunks at the expected height /// and signed by the expected miner. Returns the BlockProposal. pub fn wait_for_block_proposal( @@ -1242,12 +1312,7 @@ pub fn wait_for_block_proposal( ) -> Result { let mut proposed_block = None; wait_for(timeout_secs, || { - let chunks = test_observer::get_stackerdb_chunks(); - for chunk in chunks.into_iter().flat_map(|chunk| chunk.modified_slots) { - let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - else { - continue; - }; + for (_chunk, message) in get_stackerdb_signer_messages() { let SignerMessage::BlockProposal(proposal) = message else { continue; }; @@ -1266,32 +1331,6 @@ pub fn wait_for_block_proposal( proposed_block.ok_or_else(|| "Failed to find block proposal".to_string()) } -/// Waits for a BlockPushed to be observed in the test_observer stackerdb chunks for a block -/// with the provided signer signature hash -fn wait_for_block_pushed( - timeout_secs: u64, - block_signer_signature_hash: &Sha512Trunc256Sum, -) -> Result { - let mut block = None; - wait_for(timeout_secs, || { - let chunks = test_observer::get_stackerdb_chunks(); - for chunk in chunks.into_iter().flat_map(|chunk| chunk.modified_slots) { - let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - else { - continue; - }; - if let SignerMessage::BlockPushed(pushed_block) = message { - if &pushed_block.header.signer_signature_hash() == block_signer_signature_hash { - block = Some(pushed_block); - return Ok(true); - } - } - } - Ok(false) - })?; - block.ok_or_else(|| "Failed to find block pushed".to_string()) -} - /// Waits for a block with the provided expected height to be proposed and pushed by the miner with the provided public key. pub fn wait_for_block_pushed_by_miner_key( timeout_secs: u64, @@ -1302,12 +1341,7 @@ pub fn wait_for_block_pushed_by_miner_key( // if the signers haven't yet updated their miner viewpoint before a miner proposes a block. let mut block = None; wait_for(timeout_secs, || { - let chunks = test_observer::get_stackerdb_chunks(); - for chunk in chunks.into_iter().flat_map(|chunk| chunk.modified_slots) { - let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - else { - continue; - }; + for (_chunk, message) in get_stackerdb_signer_messages() { if let SignerMessage::BlockPushed(pushed_block) = message { let block_stacks_height = pushed_block.header.chain_length; if block_stacks_height != expected_height { @@ -1323,9 +1357,30 @@ pub fn wait_for_block_pushed_by_miner_key( } Ok(false) })?; + block.ok_or_else(|| "Failed to find block pushed".to_string()) } +/// Waits for a block to be pushed by the specified miner, then waits for +/// the node's tip to advance to that block. This prevents race conditions +/// where subsequent calls read a stale `stacks_tip_height` because the +/// coordinator hasn't yet processed the pushed block. +/// +/// `get_tip` should return the current stacks tip hash (e.g. from +/// `get_peer_info().stacks_tip` or `get_chain_info().stacks_tip`). +pub fn wait_for_block_pushed_and_tip( + timeout_secs: u64, + expected_height: u64, + expected_miner: &StacksPublicKey, + get_tip: impl Fn() -> BlockHeaderHash, +) -> Result { + let block = wait_for_block_pushed_by_miner_key(timeout_secs, expected_height, expected_miner)?; + let block_hash = block.header.block_hash(); + wait_for(timeout_secs, || Ok(get_tip() == block_hash)) + .map_err(|e| format!("Tip did not advance to pushed block: {e}"))?; + Ok(block) +} + /// Waits for all of the provided signers to send a pre-commit for a block /// with the provided signer signature hash pub fn wait_for_block_pre_commits_from_signers( @@ -1334,16 +1389,13 @@ pub fn wait_for_block_pre_commits_from_signers( expected_signers: &[StacksPublicKey], ) -> Result<(), String> { wait_for(timeout_secs, || { - let chunks = test_observer::get_stackerdb_chunks() + let chunks = get_stackerdb_signer_messages() .into_iter() - .flat_map(|chunk| chunk.modified_slots) - .filter_map(|chunk| { + .filter_map(|(chunk, message)| { let pk = chunk.recover_pk().expect("Failed to recover pk"); if !expected_signers.contains(&pk) { return None; } - let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - .expect("Failed to deserialize SignerMessage"); if let SignerMessage::BlockPreCommit(hash) = message { if hash == *signer_signature_hash { @@ -1366,12 +1418,7 @@ fn wait_for_block_global_rejection( ) -> Result<(), String> { let mut found_rejections = HashSet::new(); wait_for(timeout_secs, || { - let chunks = test_observer::get_stackerdb_chunks(); - for chunk in chunks.into_iter().flat_map(|chunk| chunk.modified_slots) { - let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - else { - continue; - }; + for (_chunk, message) in get_stackerdb_signer_messages() { if let SignerMessage::BlockResponse(BlockResponse::Rejected(BlockRejection { signer_signature_hash, signature, @@ -1397,12 +1444,7 @@ pub fn wait_for_block_global_rejection_with_reject_reason( ) -> Result<(), String> { let mut found_rejections = HashSet::new(); wait_for(timeout_secs, || { - let chunks = test_observer::get_stackerdb_chunks(); - for chunk in chunks.into_iter().flat_map(|chunk| chunk.modified_slots) { - let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - else { - continue; - }; + for (_chunk, message) in get_stackerdb_signer_messages() { if let SignerMessage::BlockResponse(BlockResponse::Rejected(BlockRejection { signer_signature_hash, signature, @@ -1425,7 +1467,7 @@ pub fn wait_for_block_global_rejection_with_reject_reason( }) } -/// Waits for the provided number of block rejections to be observed in the test_observer stackerdb chunks for a block +/// Waits for at least the provided number of block rejections to be observed in the test_observer stackerdb chunks for a block /// with the provided signer signature hash fn wait_for_block_rejections( timeout_secs: u64, @@ -1434,12 +1476,7 @@ fn wait_for_block_rejections( ) -> Result<(), String> { let mut found_rejections = HashSet::new(); wait_for(timeout_secs, || { - let chunks = test_observer::get_stackerdb_chunks(); - for chunk in chunks.into_iter().flat_map(|chunk| chunk.modified_slots) { - let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - else { - continue; - }; + for (_chunk, message) in get_stackerdb_signer_messages() { if let SignerMessage::BlockResponse(BlockResponse::Rejected(BlockRejection { signer_signature_hash, signature, @@ -1451,7 +1488,7 @@ fn wait_for_block_rejections( } } } - Ok(found_rejections.len() == num_rejections) + Ok(found_rejections.len() >= num_rejections) }) } @@ -1464,12 +1501,9 @@ pub fn wait_for_block_global_acceptance_from_signers( ) -> Result<(), String> { // Make sure that at least 70% of signers accepted the block proposal wait_for(timeout_secs, || { - let signatures = test_observer::get_stackerdb_chunks() + let signatures = get_stackerdb_signer_messages() .into_iter() - .flat_map(|chunk| chunk.modified_slots) - .filter_map(|chunk| { - let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - .expect("Failed to deserialize SignerMessage"); + .filter_map(|(_chunk, message)| { if let SignerMessage::BlockResponse(BlockResponse::Accepted(accepted)) = message { if &accepted.signer_signature_hash == signer_signature_hash && expected_signers.iter().any(|pk| { @@ -1496,12 +1530,9 @@ pub fn wait_for_block_acceptance_from_signers( ) -> Result, String> { let mut result = vec![]; wait_for(timeout_secs, || { - let signatures = test_observer::get_stackerdb_chunks() + let signatures = get_stackerdb_signer_messages() .into_iter() - .flat_map(|chunk| chunk.modified_slots) - .filter_map(|chunk| { - let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - .expect("Failed to deserialize SignerMessage"); + .filter_map(|(_chunk, message)| { if let SignerMessage::BlockResponse(BlockResponse::Accepted(accepted)) = message { if &accepted.signer_signature_hash == signer_signature_hash && expected_signers.iter().any(|pk| { @@ -1533,28 +1564,22 @@ pub fn wait_for_block_rejections_from_signers( ) -> Result, String> { let mut result = Vec::new(); wait_for(timeout_secs, || { - let stackerdb_events = test_observer::get_stackerdb_chunks(); - let block_rejections: HashMap<_, _> = stackerdb_events + let block_rejections: HashMap<_, _> = get_stackerdb_signer_messages() .into_iter() - .flat_map(|chunk| chunk.modified_slots) - .filter_map(|chunk| { - let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - .expect("Failed to deserialize SignerMessage"); - match message { - SignerMessage::BlockResponse(BlockResponse::Rejected(rejection)) => { - let rejected_pubkey = rejection - .recover_public_key() - .expect("Failed to recover public key from rejection"); - if &rejection.signer_signature_hash == signer_signature_hash - && expected_signers.contains(&rejected_pubkey) - { - Some((rejected_pubkey, rejection)) - } else { - None - } + .filter_map(|(_chunk, message)| match message { + SignerMessage::BlockResponse(BlockResponse::Rejected(rejection)) => { + let rejected_pubkey = rejection + .recover_public_key() + .expect("Failed to recover public key from rejection"); + if &rejection.signer_signature_hash == signer_signature_hash + && expected_signers.contains(&rejected_pubkey) + { + Some((rejected_pubkey, rejection)) + } else { + None } - _ => None, } + _ => None, }) .collect(); if block_rejections.len() == expected_signers.len() { @@ -1575,15 +1600,7 @@ pub fn wait_for_state_machine_update( ) -> Result<(), String> { wait_for(timeout_secs, || { let mut found_updates: HashSet = HashSet::new(); - let stackerdb_events = test_observer::get_stackerdb_chunks(); - for chunk in stackerdb_events - .into_iter() - .flat_map(|chunk| chunk.modified_slots) - { - let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - else { - continue; - }; + for (chunk, message) in get_stackerdb_signer_messages() { let SignerMessage::StateMachineUpdate(update) = message else { continue; }; @@ -1671,15 +1688,7 @@ pub fn wait_for_state_machine_update_by_miner_tenure_id( ) -> Result<(), String> { wait_for(timeout_secs, || { let mut found_updates: HashSet = HashSet::new(); - let stackerdb_events = test_observer::get_stackerdb_chunks(); - for chunk in stackerdb_events - .into_iter() - .flat_map(|chunk| chunk.modified_slots) - { - let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - else { - continue; - }; + for (chunk, message) in get_stackerdb_signer_messages() { let SignerMessage::StateMachineUpdate(update) = message else { continue; }; @@ -1826,12 +1835,7 @@ fn block_proposal_rejection() { }; while !found_signer_signature_hash_1 && !found_signer_signature_hash_2 { std::thread::sleep(Duration::from_secs(1)); - let chunks = test_observer::get_stackerdb_chunks(); - for chunk in chunks.into_iter().flat_map(|chunk| chunk.modified_slots) { - let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - else { - continue; - }; + for (_chunk, message) in get_stackerdb_signer_messages() { if let SignerMessage::BlockResponse(BlockResponse::Rejected(BlockRejection { reason: _reason, reason_code, @@ -3550,12 +3554,9 @@ fn duplicate_signers() { let start_polling = Instant::now(); while start_polling.elapsed() <= timeout { std::thread::sleep(Duration::from_secs(1)); - let messages = test_observer::get_stackerdb_chunks() + let messages = get_stackerdb_signer_messages() .into_iter() - .flat_map(|chunk| chunk.modified_slots) - .filter_map(|chunk| { - SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()).ok() - }) + .map(|(_chunk, message)| message) .filter_map(|message| match message { SignerMessage::BlockResponse(BlockResponse::Accepted(m)) => { info!("Message(accepted): {m:?}"); @@ -4078,15 +4079,10 @@ fn miner_recovers_when_broadcast_block_delay_across_tenures_occurs() { info!("Submitted tx {tx} in to mine block N"); sender_nonce += 1; let block_n = - wait_for_block_pushed_by_miner_key(30, info_before.stacks_tip_height + 1, &miner_pk) - .expect("Timed out waiting for block N to be mined"); - - let info_after = signer_test.get_peer_info(); - assert_eq!( - info_before.stacks_tip_height + 1, - info_after.stacks_tip_height - ); - assert_eq!(info_after.stacks_tip, block_n.header.block_hash()); + wait_for_block_pushed_and_tip(30, info_before.stacks_tip_height + 1, &miner_pk, || { + signer_test.get_peer_info().stacks_tip + }) + .expect("Timed out waiting for block N to be mined"); info!("------------------------- Attempt to Mine Nakamoto Block N+1 -------------------------"); // Propose a valid block, but force the miner to ignore the returned signatures and delay the block being @@ -4233,19 +4229,14 @@ fn miner_recovers_when_broadcast_block_delay_across_tenures_occurs() { info_before.stacks_tip_height + 2 ); let block_n_2 = - wait_for_block_pushed_by_miner_key(30, info_before.stacks_tip_height + 2, &miner_pk) - .expect("Timed out waiting for block N+2 to be mined"); - - let info_after = signer_test.get_peer_info(); + wait_for_block_pushed_and_tip(30, info_before.stacks_tip_height + 2, &miner_pk, || { + signer_test.get_peer_info().stacks_tip + }) + .expect("Timed out waiting for block N+2 to be mined"); assert_eq!( block_n_2.header.parent_block_id, block_n_1.header.block_id() ); - assert_eq!(info_after.stacks_tip, block_n_2.header.block_hash()); - assert_eq!( - info_before.stacks_tip_height + 2, - info_after.stacks_tip_height - ); } #[test] @@ -4690,12 +4681,7 @@ fn block_validation_response_timeout() { info!("------------------------- Wait for Block Rejection Due to Timeout -------------------------"); // Verify that the signer that submits the block to the node will issue a ConnectivityIssues rejection wait_for(30, || { - let chunks = test_observer::get_stackerdb_chunks(); - for chunk in chunks.into_iter().flat_map(|chunk| chunk.modified_slots) { - let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - else { - continue; - }; + for (_chunk, message) in get_stackerdb_signer_messages() { let SignerMessage::BlockResponse(BlockResponse::Rejected(BlockRejection { reason: _reason, reason_code, @@ -5111,14 +5097,7 @@ fn block_proposal_max_age_rejections() { // Verify the signers rejected only the SECOND block proposal. The first was not even processed. wait_for(120, || { let mut status_map = HashMap::new(); - for chunk in test_observer::get_stackerdb_chunks() - .into_iter() - .flat_map(|chunk| chunk.modified_slots) - { - let Ok(message) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - else { - continue; - }; + for (_chunk, message) in get_stackerdb_signer_messages() { match message { SignerMessage::BlockResponse(BlockResponse::Rejected(BlockRejection { signer_signature_hash, @@ -5801,12 +5780,9 @@ fn injected_signatures_are_ignored_across_boundaries() { info!("Submitted tx {tx} in attempt to mine block N"); let mut new_signature_hash = None; wait_for(30, || { - let accepted_signers: HashSet<_> = test_observer::get_stackerdb_chunks() + let accepted_signers: HashSet<_> = get_stackerdb_signer_messages() .into_iter() - .flat_map(|chunk| chunk.modified_slots) - .filter_map(|chunk| { - let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - .expect("Failed to deserialize SignerMessage"); + .filter_map(|(_chunk, message)| { if let SignerMessage::BlockResponse(BlockResponse::Accepted(accepted)) = message { new_signature_hash = Some(accepted.signer_signature_hash.clone()); return non_ignoring_signers.iter().find(|key| { @@ -5832,12 +5808,9 @@ fn injected_signatures_are_ignored_across_boundaries() { ); // Get the last block proposal - let block_proposal = test_observer::get_stackerdb_chunks() - .iter() - .flat_map(|chunk| chunk.modified_slots.clone()) - .filter_map(|chunk| { - let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - .expect("Failed to deserialize SignerMessage"); + let block_proposal = get_stackerdb_signer_messages() + .into_iter() + .filter_map(|(_chunk, message)| { if let SignerMessage::BlockProposal(proposal) = message { assert_eq!(proposal.reward_cycle, curr_reward_cycle); assert_eq!( @@ -5886,8 +5859,11 @@ fn injected_signatures_are_ignored_across_boundaries() { }) .expect("Timed out waiting for block to be mined"); - let info_after = signer_test.get_peer_info(); - assert_eq!(info_after.stacks_tip.to_string(), block.block_hash,); + wait_for(30, || { + let info = signer_test.get_peer_info(); + Ok(info.stacks_tip.to_string() == block.block_hash) + }) + .expect("Tip did not advance to block N"); // Wait 5 seconds in case there are any lingering block pushes from the signers std::thread::sleep(Duration::from_secs(5)); signer_test.shutdown(); @@ -6982,14 +6958,6 @@ fn signers_send_state_message_updates() { }, ); - let rl1_skip_commit_op = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - let rl2_skip_commit_op = miners.rl2_counters.naka_skip_commit_op.clone(); - let (conf_1, _) = miners.get_node_configs(); let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes(); let (miner_pk_1, miner_pk_2) = miners.get_miner_public_keys(); @@ -6997,7 +6965,7 @@ fn signers_send_state_message_updates() { info!("------------------------- Pause Miner 2's Block Commits -------------------------"); // Make sure Miner 2 cannot win a sortition at first. - rl2_skip_commit_op.set(true); + miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); @@ -7017,11 +6985,10 @@ fn signers_send_state_message_updates() { let starting_peer_height = get_chain_info(&conf_1).stacks_tip_height; let starting_burn_height = get_burn_height(); let mut btc_blocks_mined = 0; - info!("------------------------- Pause Miner 1's Block Commit -------------------------"); // Make sure miner 1 doesn't submit any further block commits for the next tenure BEFORE mining the bitcoin block - rl1_skip_commit_op.set(true); - + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); info!("------------------------- Miner 1 Tenure Starts and Mines Block N-------------------------"); miners .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 60) @@ -7044,7 +7011,7 @@ fn signers_send_state_message_updates() { info!("------------------------- Submit Miner 2 Block Commit -------------------------"); test_observer::clear(); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); // Pause the block proposal broadcast so that miner 2 will be unable to broadcast its // tenure change proposal BEFORE the block_proposal_timeout and will be marked invalid. @@ -7077,9 +7044,6 @@ fn signers_send_state_message_updates() { ); // Make sure that miner 2 gets marked invalid by not proposing a block BEFORE block_proposal_timeout std::thread::sleep(block_proposal_timeout.add(Duration::from_secs(1))); - // Allow miner 2 to propose its late block and see the signer get marked malicious - TEST_BROADCAST_PROPOSAL_STALL.set(vec![miner_pk_1]); - info!("------------------------- Confirm Miner 1 is the Active Miner Again -------------------------"); wait_for_state_machine_update( 60, @@ -7090,6 +7054,9 @@ fn signers_send_state_message_updates() { ) .expect("Timed out waiting for signers to send their state update"); + // Allow miner 2 to propose its late block and see the signer get marked malicious + TEST_BROADCAST_PROPOSAL_STALL.set(vec![miner_pk_1]); + info!( "------------------------- Confirm Burn and Stacks Block Heights -------------------------" ); @@ -7545,8 +7512,9 @@ fn miner_stackerdb_version_rollover() { let sortdb = burnchain.open_sortition_db(true).unwrap(); info!("------------------------- Pause Miner 1's Block Commit -------------------------"); - - // Make sure miner 1 doesn't submit any further block commits for the next tenure BEFORE mining the bitcoin block + // Make sure the miner has submitted a commit for the latest burn block and also make sure it pauses + // before mining the bitcoin block so that the miner won't accidentally extend its tenure before we pause it. + miners.ensure_commit_miner_1(&sortdb); miners.pause_commits_miner_1(); info!("------------------------- Miner 1 Wins Normal Tenure A -------------------------"); @@ -7586,7 +7554,7 @@ fn miner_stackerdb_version_rollover() { let max_chunk = max_chunk.expect("Should have found a miner stackerdb message from Miner 1"); info!("------------------------- Miner 2 Wins Tenure B -------------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); miners .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30) @@ -7594,7 +7562,7 @@ fn miner_stackerdb_version_rollover() { verify_sortition_winner(&sortdb, &miner_pkh_2); info!("------------------------- Miner 2 Wins Tenure C -------------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); miners .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30) @@ -7602,7 +7570,7 @@ fn miner_stackerdb_version_rollover() { verify_sortition_winner(&sortdb, &miner_pkh_2); info!("------------------------- Miner 2 Wins Tenure D -------------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); miners .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30) @@ -7610,7 +7578,7 @@ fn miner_stackerdb_version_rollover() { verify_sortition_winner(&sortdb, &miner_pkh_2); info!("----------------- Miner 1 Submits Block Commit ------------------"); - miners.submit_commit_miner_1(&sortdb); + miners.ensure_commit_miner_1(&sortdb); info!("------------------------- Miner 1 Wins Tenure E -------------------------"); miners @@ -7748,13 +7716,7 @@ fn multiversioned_signer_protocol_version_calculation() { info!("------------------------- Verifying Signers ONLY Sends Acceptances -------------------------"); wait_for(30, || { let mut nmb_accept = 0; - let stackerdb_events = test_observer::get_stackerdb_chunks(); - for chunk in stackerdb_events - .into_iter() - .flat_map(|chunk| chunk.modified_slots) - { - let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - .expect("Failed to deserialize SignerMessage"); + for (_chunk, message) in get_stackerdb_signer_messages() { let SignerMessage::BlockResponse(response) = message else { continue; }; @@ -7891,9 +7853,9 @@ fn signer_loads_stackerdb_updates_on_startup() { .signer_test .running_nodes .counters - .naka_skip_commit_op + .skip_commit_op .clone(); - let skip_commit_op_rl2 = miners.rl2_counters.naka_skip_commit_op.clone(); + let skip_commit_op_rl2 = miners.rl2_counters.skip_commit_op.clone(); let (conf_1, _conf_2) = miners.get_node_configs(); let (miner_pk_1, miner_pk_2) = miners.get_miner_public_keys(); @@ -7938,7 +7900,7 @@ fn signer_loads_stackerdb_updates_on_startup() { .expect("Not all signers accepted the block"); info!("------------------------- Miner B Wins Tenure B -------------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); // Let's not mine anything until we see consensus on new tenure start. TEST_MINE_SKIP.set(true); miners.signer_test.mine_bitcoin_block(); @@ -8067,12 +8029,7 @@ fn signers_do_not_commit_unless_threshold_precommitted() { .expect("Timed out waiting for pre-commits"); assert!( wait_for(30, || { - for chunk in test_observer::get_stackerdb_chunks() - .into_iter() - .flat_map(|chunk| chunk.modified_slots) - { - let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - .expect("Failed to deserialize SignerMessage"); + for (_chunk, message) in get_stackerdb_signer_messages() { if let SignerMessage::BlockResponse(BlockResponse::Accepted(accepted)) = message { if accepted.signer_signature_hash == hash { return Ok(true); @@ -8133,6 +8090,7 @@ fn signers_treat_signatures_as_precommits() { "------------------------- Trigger Tenure Change Block Proposal -------------------------" ); signer_test.mine_bitcoin_block(); + signer_test.wait_for_signer_state_update(); let block_proposal = wait_for_block_proposal_block(30, peer_info.stacks_tip_height + 1, &miner_pk) @@ -8225,12 +8183,7 @@ fn signers_treat_signatures_as_precommits() { info!("------------------------- Verifying Operating Signer Issues a Signature ------------------------"); } let result = wait_for(20, || { - for chunk in test_observer::get_stackerdb_chunks() - .into_iter() - .flat_map(|chunk| chunk.modified_slots) - { - let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - .expect("Failed to deserialize SignerMessage"); + for (_chunk, message) in get_stackerdb_signer_messages() { let SignerMessage::BlockResponse(BlockResponse::Accepted(accepted)) = message else { continue; @@ -8290,6 +8243,10 @@ fn burn_block_payload_includes_pox_transactions() { info!("---- Starting test -----"); + // Ensure both miners have submitted commits before mining the next BTC block + miners.ensure_commit_miner_1(&sortdb); + miners.ensure_commit_miner_2(&sortdb); + miners .mine_bitcoin_blocks_and_confirm(&sortdb, 1, 30) .expect("Failed to mine BTC block."); diff --git a/stacks-node/src/tests/signer/v0/reorg.rs b/stacks-node/src/tests/signer/v0/reorg.rs index 7bd50ab2372..2366278eff0 100644 --- a/stacks-node/src/tests/signer/v0/reorg.rs +++ b/stacks-node/src/tests/signer/v0/reorg.rs @@ -235,17 +235,18 @@ fn reorg_attempts_count_towards_miner_validity() { ); // The signer should automatically attempt to mine a new block once the signers eventually tell it to abandon the previous block // It will accept it even though block proposal timeout is exceeded because the miner did manage to propose block N' BEFORE the timeout. - let block_n_1 = - wait_for_block_pushed_by_miner_key(30, block_proposal_n.header.chain_length + 1, &miner_pk) - .expect("Failed to get mined block N+1"); + let block_n_1 = wait_for_block_pushed_and_tip( + 30, + block_proposal_n.header.chain_length + 1, + &miner_pk, + || get_chain_info(&signer_test.running_nodes.conf).stacks_tip, + ) + .expect("Failed to get mined block N+1"); assert!(block_n_1 .get_tenure_tx_payload() .unwrap() .cause .is_eq(&TenureChangeCause::BlockFound),); - let chain_after = get_chain_info(&signer_test.running_nodes.conf); - - assert_eq!(chain_after.stacks_tip, block_n_1.header.block_hash()); assert_eq!( block_n_1.header.chain_length, block_proposal_n_prime.header.chain_length + 1 @@ -504,13 +505,6 @@ fn allow_reorg_within_first_proposal_burn_block_timing_secs() { config.miner.block_commit_delay = Duration::from_secs(0); }, ); - let rl1_skip_commit_op = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - let rl2_skip_commit_op = miners.rl2_counters.naka_skip_commit_op.clone(); let (conf_1, _) = miners.get_node_configs(); let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes(); @@ -519,7 +513,7 @@ fn allow_reorg_within_first_proposal_burn_block_timing_secs() { info!("------------------------- Pause Miner 2's Block Commits -------------------------"); // Make sure Miner 2 cannot win a sortition at first. - rl2_skip_commit_op.set(true); + miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); @@ -527,7 +521,8 @@ fn allow_reorg_within_first_proposal_burn_block_timing_secs() { let sortdb = burnchain.open_sortition_db(true).unwrap(); info!("------------------------- Pause Miner 1's Block Commits -------------------------"); - rl1_skip_commit_op.set(true); + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); info!("------------------------- Miner 1 Mines a Nakamoto Block N -------------------------"); let stacks_height_before = miners.get_peer_stacks_tip_height(); @@ -536,21 +531,20 @@ fn allow_reorg_within_first_proposal_burn_block_timing_secs() { .expect("Failed to mine BTC block followed by Block N"); let miner_1_block_n = - wait_for_block_pushed_by_miner_key(30, stacks_height_before + 1, &miner_pk_1) - .expect("Failed to get block N"); + wait_for_block_pushed_and_tip(30, stacks_height_before + 1, &miner_pk_1, || { + get_chain_info(&conf_1).stacks_tip + }) + .expect("Failed to get block N"); let block_n_height = miner_1_block_n.header.chain_length; info!("Block N: {block_n_height}"); - let info_after = get_chain_info(&conf_1); - assert_eq!(info_after.stacks_tip, miner_1_block_n.header.block_hash()); - assert_eq!(info_after.stacks_tip_height, block_n_height); assert_eq!(block_n_height, stacks_height_before + 1); // assure we have a successful sortition that miner 1 won verify_sortition_winner(&sortdb, &miner_pkh_1); info!("------------------------- Miner 2 Submits a Block Commit -------------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); info!("------------------------- Pause Miner 2's Block Proposals -------------------------"); TEST_BROADCAST_PROPOSAL_STALL.set(vec![miner_pk_2.clone()]); @@ -564,19 +558,17 @@ fn allow_reorg_within_first_proposal_burn_block_timing_secs() { verify_sortition_winner(&sortdb, &miner_pkh_2); info!("------------------------- Miner 1 Submits a Block Commit -------------------------"); - miners.submit_commit_miner_1(&sortdb); + miners.ensure_commit_miner_1(&sortdb); info!("------------------------- Miner 2 Mines Block N+1 -------------------------"); test_observer::clear(); TEST_BROADCAST_PROPOSAL_STALL.set(vec![miner_pk_1.clone()]); - let miner_2_block_n_1 = wait_for_block_pushed_by_miner_key(30, block_n_height + 1, &miner_pk_2) + let miner_2_block_n_1 = + wait_for_block_pushed_and_tip(30, block_n_height + 1, &miner_pk_2, || { + get_chain_info(&conf_1).stacks_tip + }) .expect("Failed to get block N+1"); - assert_eq!( - get_chain_info(&conf_1).stacks_tip_height, - block_n_height + 1 - ); - info!("------------------------- Miner 1 Wins the Next Tenure, Mines N+1' -------------------------"); TEST_BROADCAST_PROPOSAL_STALL.set(vec![miner_pk_2]); miners @@ -586,18 +578,23 @@ fn allow_reorg_within_first_proposal_burn_block_timing_secs() { verify_sortition_winner(&sortdb, &miner_pkh_1); TEST_BROADCAST_PROPOSAL_STALL.set(vec![]); let miner_1_block_n_1_prime = - wait_for_block_pushed_by_miner_key(30, block_n_height + 1, &miner_pk_1) - .expect("Failed to get block N+1'"); + wait_for_block_pushed_and_tip(30, block_n_height + 1, &miner_pk_1, || { + miners.get_peer_info().stacks_tip + }) + .expect("Failed to get block N+1'"); assert_ne!(miner_1_block_n_1_prime, miner_2_block_n_1); info!("------------------------- Miner 1 Submits a Block Commit -------------------------"); - miners.submit_commit_miner_1(&sortdb); + miners.ensure_commit_miner_1(&sortdb); info!("------------------------- Miner 1 Mines N+2' -------------------------"); // Cannot use send_and_mine_transfer_tx as this relies on the peer's height miners.send_transfer_tx(); - let _ = wait_for_block_pushed_by_miner_key(30, block_n_height + 2, &miner_pk_1) + let _miner_1_block_n_2_prime = + wait_for_block_pushed_and_tip(30, block_n_height + 2, &miner_pk_1, || { + miners.get_peer_info().stacks_tip + }) .expect("Failed to get block N+2'"); info!("------------------------- Miner 1 Mines N+3 in Next Tenure -------------------------"); @@ -605,13 +602,12 @@ fn allow_reorg_within_first_proposal_burn_block_timing_secs() { miners .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 60) .expect("Failed to mine BTC block followed by Block N+2"); - let miner_1_block_n_3 = wait_for_block_pushed_by_miner_key(30, block_n_height + 3, &miner_pk_1) + let _miner_1_block_n_3 = + wait_for_block_pushed_and_tip(30, block_n_height + 3, &miner_pk_1, || { + miners.get_peer_info().stacks_tip + }) .expect("Failed to get block N+3"); - let peer_info = miners.get_peer_info(); - assert_eq!(peer_info.stacks_tip_height, block_n_height + 3); - assert_eq!(peer_info.stacks_tip, miner_1_block_n_3.header.block_hash()); - miners.shutdown(); } @@ -678,16 +674,13 @@ fn disallow_reorg_within_first_proposal_burn_block_timing_secs_but_more_than_one signer_config.tenure_last_block_proposal_timeout = Duration::from_secs(1800); signer_config.first_proposal_burn_block_timing = Duration::from_secs(1800); }, - |_| {}, - |_| {}, + |config| { + config.miner.block_commit_delay = Duration::from_secs(0); + }, + |config| { + config.miner.block_commit_delay = Duration::from_secs(0); + }, ); - let rl1_skip_commit_op = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - let rl2_skip_commit_op = miners.rl2_counters.naka_skip_commit_op.clone(); let (conf_1, _) = miners.get_node_configs(); let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes(); @@ -696,7 +689,7 @@ fn disallow_reorg_within_first_proposal_burn_block_timing_secs_but_more_than_one info!("------------------------- Pause Miner 2's Block Commits -------------------------"); // Make sure Miner 2 cannot win a sortition at first. - rl2_skip_commit_op.set(true); + miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); @@ -704,7 +697,9 @@ fn disallow_reorg_within_first_proposal_burn_block_timing_secs_but_more_than_one let sortdb = burnchain.open_sortition_db(true).unwrap(); info!("------------------------- Pause Miner 1's Block Commits -------------------------"); - rl1_skip_commit_op.set(true); + // Before pausing, make sure the miner is pointing to the right sortition DB state. + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); info!("------------------------- Miner 1 Mines a Nakamoto Block N -------------------------"); let stacks_height_before = miners.get_peer_stacks_tip_height(); @@ -713,21 +708,20 @@ fn disallow_reorg_within_first_proposal_burn_block_timing_secs_but_more_than_one .expect("Failed to mine BTC block followed by Block N"); let miner_1_block_n = - wait_for_block_pushed_by_miner_key(30, stacks_height_before + 1, &miner_pk_1) - .expect("Failed to get block N"); + wait_for_block_pushed_and_tip(30, stacks_height_before + 1, &miner_pk_1, || { + get_chain_info(&conf_1).stacks_tip + }) + .expect("Failed to get block N"); let block_n_height = miner_1_block_n.header.chain_length; info!("Block N: {block_n_height}"); - let info_after = get_chain_info(&conf_1); - assert_eq!(info_after.stacks_tip, miner_1_block_n.header.block_hash()); - assert_eq!(info_after.stacks_tip_height, block_n_height); assert_eq!(block_n_height, stacks_height_before + 1); // assure we have a successful sortition that miner 1 won verify_sortition_winner(&sortdb, &miner_pkh_1); info!("------------------------- Miner 2 Submits a Block Commit -------------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); info!("------------------------- Pause Miner 2's Block Mining -------------------------"); fault_injection_stall_miner(); @@ -738,22 +732,19 @@ fn disallow_reorg_within_first_proposal_burn_block_timing_secs_but_more_than_one .expect("Failed to mine BTC block"); info!("------------------------- Miner 1 Submits a Block Commit -------------------------"); - miners.submit_commit_miner_1(&sortdb); + miners.ensure_commit_miner_1(&sortdb); info!("------------------------- Miner 2 Mines Block N+1 -------------------------"); fault_injection_unstall_miner(); - let _ = wait_for_block_pushed_by_miner_key(30, block_n_height + 1, &miner_pk_2) - .expect("Failed to get block N+1"); + let _ = wait_for_block_pushed_and_tip(30, block_n_height + 1, &miner_pk_2, || { + get_chain_info(&conf_1).stacks_tip + }) + .expect("Failed to get block N+1"); // assure we have a successful sortition that miner 2 won verify_sortition_winner(&sortdb, &miner_pkh_2); - assert_eq!( - get_chain_info(&conf_1).stacks_tip_height, - block_n_height + 1 - ); - info!("------------------------- Miner 2 Mines N+2 and N+3 -------------------------"); miners .send_and_mine_transfer_tx(30) @@ -767,6 +758,7 @@ fn disallow_reorg_within_first_proposal_burn_block_timing_secs_but_more_than_one ); info!("------------------------- Miner 1 Wins the Next Tenure, Mines N+1', got rejected -------------------------"); miners.signer_test.mine_bitcoin_block(); + miners.signer_test.wait_for_signer_state_update(); // assure we have a successful sortition that miner 1 won verify_sortition_winner(&sortdb, &miner_pkh_1); // wait for a block N+1' proposal from miner1 @@ -889,14 +881,6 @@ fn interrupt_miner_on_new_stacks_tip() { }, ); - let skip_commit_op_rl1 = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - let skip_commit_op_rl2 = miners.rl2_counters.naka_skip_commit_op.clone(); - let (conf_1, conf_2) = miners.get_node_configs(); let (miner_pk_1, miner_pk_2) = miners.get_miner_public_keys(); let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes(); @@ -904,13 +888,14 @@ fn interrupt_miner_on_new_stacks_tip() { let all_signers = miners.signer_test.signer_test_pks(); // Pause Miner 2's commits to ensure Miner 1 wins the first sortition. - skip_commit_op_rl2.set(true); + miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); let sortdb = conf_1.get_burnchain().open_sortition_db(true).unwrap(); + miners.ensure_commit_miner_1(&sortdb); info!("Pausing miner 1's block commit submissions"); - skip_commit_op_rl1.set(true); + miners.pause_commits_miner_1(); info!("------------------------- RL1 Wins Sortition -------------------------"); info!("Mine RL1 Tenure"); @@ -949,7 +934,7 @@ fn interrupt_miner_on_new_stacks_tip() { info!("Block N is {}", block_n.stacks_height); info!("------------------------- RL2 Wins Sortition -------------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); info!("Make signers ignore all block proposals, so that they don't reject it quickly"); TEST_IGNORE_ALL_BLOCK_PROPOSALS.set(all_signers.clone()); @@ -1000,17 +985,15 @@ fn interrupt_miner_on_new_stacks_tip() { ); info!("------------------------- Signers Accept Block N+1 -------------------------"); - let miner_2_block_n_1 = - wait_for_block_pushed(30, &miner_2_block_n_1.header.signer_signature_hash()) - .expect("Failed to see block acceptance of Miner 2's Block N+1"); - assert_eq!( - miner_2_block_n_1.header.block_hash(), - miners.get_peer_stacks_tip() - ); + let _miner_2_block_n_1 = + wait_for_block_pushed_and_tip(30, stacks_height_before + 2, &miner_pk_2, || { + miners.get_peer_info().stacks_tip + }) + .expect("Failed to see block acceptance of Miner 2's Block N+1"); info!("------------------------- Next Tenure Builds on N+1 -------------------------"); - miners.submit_commit_miner_1(&sortdb); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_1(&sortdb); + miners.ensure_commit_miner_2(&sortdb); miners .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30) @@ -1123,15 +1106,10 @@ fn global_acceptance_depends_on_block_announcement() { // Ensure that the block was accepted globally so the stacks tip has advanced to N let block_n = - wait_for_block_pushed_by_miner_key(30, info_before.stacks_tip_height + 1, &miner_pk) - .expect("Timed out waiting for block N to be mined"); - - let info_after = signer_test.get_peer_info(); - assert_eq!(info_after.stacks_tip, block_n.header.block_hash()); - assert_eq!( - info_after.stacks_tip_height, - info_before.stacks_tip_height + 1 - ); + wait_for_block_pushed_and_tip(30, info_before.stacks_tip_height + 1, &miner_pk, || { + signer_test.get_peer_info().stacks_tip + }) + .expect("Timed out waiting for block N to be mined"); info!("------------------------- Mine Nakamoto Block N+1 -------------------------"); // Make less than 30% of the signers reject the block and ensure it is accepted by the node, but not announced. @@ -1198,8 +1176,10 @@ fn global_acceptance_depends_on_block_announcement() { info!("------------------------- Waiting for block N+1' -------------------------"); // Cannot use wait_for_block_pushed_by_miner_key as we could have more than one block proposal for the same height from the miner let sister_block = - wait_for_block_pushed_by_miner_key(30, info_before.stacks_tip_height + 1, &miner_pk) - .expect("Failed to get pushed sister block"); + wait_for_block_pushed_and_tip(30, info_before.stacks_tip_height + 1, &miner_pk, || { + signer_test.get_peer_info().stacks_tip + }) + .expect("Failed to get pushed sister block"); assert_ne!( sister_block.header.signer_signature_hash(), block_n_1.header.signer_signature_hash() @@ -1208,14 +1188,8 @@ fn global_acceptance_depends_on_block_announcement() { sister_block.header.chain_length, block_n_1.header.chain_length ); - // Assert the block was mined and the tip has changed. let info_after = signer_test.get_peer_info(); - assert_eq!( - info_after.stacks_tip_height, - sister_block.header.chain_length - ); - assert_eq!(info_after.stacks_tip, sister_block.header.block_hash()); assert_eq!( info_after.stacks_tip_consensus_hash, sister_block.header.consensus_hash @@ -1288,12 +1262,6 @@ fn no_reorg_due_to_successive_block_validation_ok() { let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes(); let (miner_pk_1, miner_pk_2) = miners.get_miner_public_keys(); - let rl1_skip_commit_op = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); let blocks_mined1 = miners .signer_test .running_nodes @@ -1302,7 +1270,6 @@ fn no_reorg_due_to_successive_block_validation_ok() { .clone(); let Counters { - naka_skip_commit_op: rl2_skip_commit_op, naka_mined_blocks: blocks_mined2, naka_rejected_blocks: rl2_rejections, .. @@ -1311,7 +1278,7 @@ fn no_reorg_due_to_successive_block_validation_ok() { info!("------------------------- Pause Miner 2's Block Commits -------------------------"); // Make sure Miner 2 cannot win a sortition at first. - rl2_skip_commit_op.set(true); + miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); @@ -1321,7 +1288,8 @@ fn no_reorg_due_to_successive_block_validation_ok() { let starting_peer_height = get_chain_info(&conf_1).stacks_tip_height; info!("------------------------- Pause Miner 1's Block Commits -------------------------"); - rl1_skip_commit_op.set(true); + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); info!("------------------------- Miner 1 Mines a Nakamoto Block N (Globally Accepted) -------------------------"); let stacks_height_before = miners.get_peer_stacks_tip_height(); @@ -1330,11 +1298,11 @@ fn no_reorg_due_to_successive_block_validation_ok() { .expect("Failed to mine Block N"); // assure we have a successful sortition that miner 1 won verify_sortition_winner(&sortdb, &miner_pkh_1); - let block_n = wait_for_block_pushed_by_miner_key(30, stacks_height_before + 1, &miner_pk_1) - .expect("Failed to find block N"); + let block_n = wait_for_block_pushed_and_tip(30, stacks_height_before + 1, &miner_pk_1, || { + miners.get_peer_info().stacks_tip + }) + .expect("Failed to find block N"); let block_n_signature_hash = block_n.header.signer_signature_hash(); - - assert_eq!(miners.get_peer_stacks_tip(), block_n.header.block_hash()); debug!("Miner 1 mined block N: {block_n_signature_hash}"); info!("------------------------- Pause Block Validation Response of N+1 -------------------------"); @@ -1365,7 +1333,7 @@ fn no_reorg_due_to_successive_block_validation_ok() { debug!("Miner 1 proposed block N+1: {block_n_1_signature_hash}"); info!("------------------------- Unpause Miner 2's Block Commits -------------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); info!("------------------------- Pause Block Validation Submission of N+1'-------------------------"); TEST_STALL_BLOCK_VALIDATION_SUBMISSION.set(true); @@ -1461,14 +1429,13 @@ fn no_reorg_due_to_successive_block_validation_ok() { // Miner 2 will see block N+1 as a valid block and reattempt to mine N+2 on top. info!("------------------------- Confirm N+2 Accepted ------------------------"); let block_n_2 = - wait_for_block_pushed_by_miner_key(30, block_n_1.header.chain_length + 1, &miner_pk_2) - .expect("Failed to find block N+2"); - assert_eq!(miners.get_peer_stacks_tip(), block_n_2.header.block_hash()); + wait_for_block_pushed_and_tip(30, block_n_1.header.chain_length + 1, &miner_pk_2, || { + get_chain_info(&conf_1).stacks_tip + }) + .expect("Failed to find block N+2"); info!("------------------------- Confirm Stacks Chain is As Expected ------------------------"); let info_after = get_chain_info(&conf_1); - assert_eq!(info_after.stacks_tip_height, block_n_2.header.chain_length); assert_eq!(info_after.stacks_tip_height, starting_peer_height + 3); - assert_eq!(info_after.stacks_tip, block_n_2.header.block_hash()); assert_ne!( info_after.stacks_tip_consensus_hash, block_n_1.header.consensus_hash @@ -1710,7 +1677,7 @@ fn forked_tenure_testing( naka_submitted_commits: commits_submitted, naka_mined_blocks: mined_blocks, naka_proposed_blocks: proposed_blocks, - naka_skip_commit_op: skip_commit_op, + skip_commit_op, .. } = signer_test.running_nodes.counters.clone(); @@ -1838,11 +1805,14 @@ fn forked_tenure_testing( // Submit a block commit op for tenure C let commits_before = commits_submitted.load(Ordering::SeqCst); - let blocks_before = if expect_tenure_c { - mined_blocks.load(Ordering::SeqCst) - } else { - proposed_blocks.load(Ordering::SeqCst) + let get_blocks_count = || { + if expect_tenure_c { + mined_blocks.load(Ordering::SeqCst) + } else { + proposed_blocks.load(Ordering::SeqCst) + } }; + let blocks_before = get_blocks_count(); skip_commit_op.set(false); next_block_and( @@ -1854,7 +1824,10 @@ fn forked_tenure_testing( // now allow block B to process if it hasn't already. TEST_BLOCK_ANNOUNCE_STALL.set(false); } - let blocks_count = mined_blocks.load(Ordering::SeqCst); + // When we don't expect tenure C to produce a valid block, + // check proposed_blocks (the miner will propose but signers + // will reject). When we do expect it, check mined_blocks. + let blocks_count = get_blocks_count(); let rbf_count = if expect_tenure_c { 1 } else { 0 }; Ok(commits_count > commits_before + rbf_count && blocks_count > blocks_before) @@ -1862,7 +1835,7 @@ fn forked_tenure_testing( ) .unwrap_or_else(|_| { let commits_count = commits_submitted.load(Ordering::SeqCst); - let blocks_count = mined_blocks.load(Ordering::SeqCst); + let blocks_count = get_blocks_count(); let rbf_count = if expect_tenure_c { 1 } else { 0 }; error!("Tenure C failed to produce a block"; "commits_count" => commits_count, @@ -2342,7 +2315,7 @@ fn partial_tenure_fork() { let rl2_coord_channels = run_loop_2.coordinator_channels(); let run_loop_stopper_2 = run_loop_2.get_termination_switch(); let Counters { - naka_skip_commit_op: rl2_skip_commit_op, + skip_commit_op: rl2_skip_commit_op, .. } = run_loop_2.counters(); let rl2_counters = run_loop_2.counters(); @@ -2372,11 +2345,7 @@ fn partial_tenure_fork() { info!("------------------------- Reached Epoch 3.0 -------------------------"); - let rl1_skip_commit_op = signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); + let rl1_skip_commit_op = signer_test.running_nodes.counters.skip_commit_op.clone(); let sortdb = SortitionDB::open( &conf.get_burn_db_file_path(), @@ -2629,15 +2598,11 @@ fn locally_accepted_blocks_overriden_by_global_rejection() { let tx = submit_tx(&http_origin, &transfer_tx); sender_nonce += 1; info!("Submitted tx {tx} in to mine block N"); - let block_n = - wait_for_block_pushed_by_miner_key(30, info_before.stacks_tip_height + 1, &miner_pk) - .expect("Timed out waiting for block N to be mined"); - let info_after = signer_test.get_peer_info(); - assert_eq!( - info_before.stacks_tip_height + 1, - info_after.stacks_tip_height - ); - assert_eq!(info_after.stacks_tip, block_n.header.block_hash()); + let _block_n = + wait_for_block_pushed_and_tip(30, info_before.stacks_tip_height + 1, &miner_pk, || { + signer_test.get_peer_info().stacks_tip + }) + .expect("Timed out waiting for block N to be mined"); info!("------------------------- Attempt to Mine Nakamoto Block N+1 -------------------------"); // Make half of the signers reject the block proposal by the miner to ensure its marked globally rejected @@ -2690,19 +2655,13 @@ fn locally_accepted_blocks_overriden_by_global_rejection() { let tx = submit_tx(&http_origin, &transfer_tx); info!("Submitted tx {tx} to mine block N+1'"); - let block_n_1_prime = wait_for_block_pushed_by_miner_key( + let block_n_1_prime = wait_for_block_pushed_and_tip( short_timeout_secs, info_before.stacks_tip_height + 1, &miner_pk, + || signer_test.get_peer_info().stacks_tip, ) .expect("Timed out waiting for block N+1' to be mined"); - - let info_after = signer_test.get_peer_info(); - assert_eq!( - info_after.stacks_tip_height, - info_before.stacks_tip_height + 1 - ); - assert_eq!(info_after.stacks_tip, block_n_1_prime.header.block_hash()); assert_ne!(block_n_1_prime, proposed_block_n_1); signer_test.shutdown(); @@ -2782,15 +2741,11 @@ fn locally_rejected_blocks_overriden_by_global_acceptance() { let tx = submit_tx(&http_origin, &transfer_tx); sender_nonce += 1; info!("Submitted tx {tx} in to mine block N"); - let block_n = - wait_for_block_pushed_by_miner_key(30, info_before.stacks_tip_height + 1, &miner_pk) - .expect("Timed out waiting for block N to be mined"); - let info_after = signer_test.get_peer_info(); - assert_eq!( - info_after.stacks_tip_height, - info_before.stacks_tip_height + 1 - ); - assert_eq!(info_after.stacks_tip, block_n.header.block_hash()); + let _block_n = + wait_for_block_pushed_and_tip(30, info_before.stacks_tip_height + 1, &miner_pk, || { + signer_test.get_peer_info().stacks_tip + }) + .expect("Timed out waiting for block N to be mined"); info!("------------------------- Mine Nakamoto Block N+1 -------------------------"); // Make less than 30% of the signers reject the block and ensure it is STILL marked globally accepted @@ -2817,8 +2772,10 @@ fn locally_rejected_blocks_overriden_by_global_acceptance() { info!("Submitted tx {tx} in to mine block N+1"); // The rejecting signers will reject the block, but it will still be accepted globally let block_n_1 = - wait_for_block_pushed_by_miner_key(30, info_before.stacks_tip_height + 1, &miner_pk) - .expect("Timed out waiting for block N+1 to be mined"); + wait_for_block_pushed_and_tip(30, info_before.stacks_tip_height + 1, &miner_pk, || { + signer_test.get_peer_info().stacks_tip + }) + .expect("Timed out waiting for block N+1 to be mined"); wait_for_block_rejections_from_signers( short_timeout, @@ -2827,14 +2784,6 @@ fn locally_rejected_blocks_overriden_by_global_acceptance() { ) .expect("Timed out waiting for block rejection of N+1"); - // Assert the block was mined and the tip advanced to N+1 - let info_after = signer_test.get_peer_info(); - assert_eq!(info_after.stacks_tip, block_n_1.header.block_hash()); - assert_eq!( - info_after.stacks_tip_height, - info_before.stacks_tip_height + 1 - ); - info!("------------------------- Test Mine Nakamoto Block N+2 -------------------------"); // Ensure that all signers accept the block proposal N+2 let info_before = signer_test.get_peer_info(); @@ -2851,15 +2800,16 @@ fn locally_rejected_blocks_overriden_by_global_acceptance() { ); let tx = submit_tx(&http_origin, &transfer_tx); info!("Submitted tx {tx} in to mine block N+2"); - let block_n_2 = - wait_for_block_pushed_by_miner_key(30, info_before.stacks_tip_height + 1, &miner_pk) - .expect("Timed out waiting for block N+2 to be pushed"); + let _block_n_2 = + wait_for_block_pushed_and_tip(30, info_before.stacks_tip_height + 1, &miner_pk, || { + signer_test.get_peer_info().stacks_tip + }) + .expect("Timed out waiting for block N+2 to be pushed"); let info_after = signer_test.get_peer_info(); assert_eq!( info_before.stacks_tip_height + 1, info_after.stacks_tip_height, ); - assert_eq!(info_after.stacks_tip, block_n_2.header.block_hash()); signer_test.shutdown(); } @@ -2938,19 +2888,14 @@ fn reorg_locally_accepted_blocks_across_tenures_succeeds() { let txid = submit_tx(&http_origin, &transfer_tx); sender_nonce += 1; let block_n = - wait_for_block_pushed_by_miner_key(30, info_before.stacks_tip_height + 1, &miner_pk) - .expect("Timed out waiting for block N to be mined"); + wait_for_block_pushed_and_tip(30, info_before.stacks_tip_height + 1, &miner_pk, || { + signer_test.get_peer_info().stacks_tip + }) + .expect("Timed out waiting for block N to be mined"); assert!(block_n .txs .iter() .any(|tx| { tx.txid().to_string() == txid })); - // Ensure that the block was accepted globally so the stacks tip has advanced to N - let info_after = signer_test.get_peer_info(); - assert_eq!( - info_before.stacks_tip_height + 1, - info_after.stacks_tip_height - ); - assert_eq!(info_after.stacks_tip, block_n.header.block_hash()); info!("------------------------- Attempt to Mine Nakamoto Block N+1 at Height {} -------------------------", info_before.stacks_tip_height + 2); // Make more than >70% of the signers ignore the block proposal to ensure it it is not globally accepted/rejected @@ -3036,15 +2981,10 @@ fn reorg_locally_accepted_blocks_across_tenures_succeeds() { TEST_MINE_SKIP.set(false); let block_n_1_prime = - wait_for_block_pushed_by_miner_key(30, info_before.stacks_tip_height + 1, &miner_pk) - .expect("Timed out waiting for block N+1' to be mined"); - // Ensure that the block was accepted globally so the stacks tip has advanced to N+1' (even though they signed a sister block in the prior tenure) - let info_after = signer_test.get_peer_info(); - assert_eq!( - info_before.stacks_tip_height + 1, - info_after.stacks_tip_height - ); - assert_eq!(info_after.stacks_tip, block_n_1_prime.header.block_hash()); + wait_for_block_pushed_and_tip(30, info_before.stacks_tip_height + 1, &miner_pk, || { + signer_test.get_peer_info().stacks_tip + }) + .expect("Timed out waiting for block N+1' to be mined"); assert_ne!( block_n_1_prime.header.signer_signature_hash(), block_n_1_proposal.header.signer_signature_hash() @@ -3059,21 +2999,10 @@ fn reorg_locally_accepted_blocks_across_tenures_succeeds() { info_before.stacks_tip_height + 2 ); let block_n_2 = - wait_for_block_pushed_by_miner_key(30, info_before.stacks_tip_height + 2, &miner_pk) - .expect("Timed out waiting for block N+2 to be mined"); - - wait_for(30, || { - let info = signer_test.get_peer_info(); - Ok(info.stacks_tip_height > info_before.stacks_tip_height + 1) - }) - .expect("Timed out waiting for the chain tip to advance"); - // Ensure that the block was accepted globally so the stacks tip has advanced to N+2 (built on N+1' even though they signed a sister block in the prior tenure) - let info_after = signer_test.get_peer_info(); - assert_eq!( - info_before.stacks_tip_height + 2, - info_after.stacks_tip_height - ); - assert_eq!(info_after.stacks_tip, block_n_2.header.block_hash()); + wait_for_block_pushed_and_tip(30, info_before.stacks_tip_height + 2, &miner_pk, || { + signer_test.get_peer_info().stacks_tip + }) + .expect("Timed out waiting for block N+2 to be mined"); assert_eq!(block_n_2.header.parent_block_id, block_n_1_prime.block_id()); signer_test.shutdown(); } @@ -3154,21 +3083,11 @@ fn reorg_locally_accepted_blocks_across_tenures_fails() { let tx = submit_tx(&http_origin, &transfer_tx); sender_nonce += 1; info!("Submitted tx {tx} in to mine block N"); - let block_n = - wait_for_block_pushed_by_miner_key(30, info_before.stacks_tip_height + 1, &miner_pk) - .expect("Timed out waiting for block N to be mined"); - // Due to a potential race condition in processing and block pushed...have to wait - wait_for(30, || { - Ok(signer_test.get_peer_info().stacks_tip_height > info_before.stacks_tip_height) - }) - .expect("Stacks tip failed to advance"); - // Ensure that the block was accepted globally so the stacks tip has advanced to N - let info_after = signer_test.get_peer_info(); - assert_eq!( - info_before.stacks_tip_height + 1, - info_after.stacks_tip_height - ); - assert_eq!(info_after.stacks_tip, block_n.header.block_hash()); + let _block_n = + wait_for_block_pushed_and_tip(30, info_before.stacks_tip_height + 1, &miner_pk, || { + signer_test.get_peer_info().stacks_tip + }) + .expect("Timed out waiting for block N to be mined"); info!("------------------------- Attempt to Mine Nakamoto Block N+1 -------------------------"); // Make more than >70% of the signers ignore the block proposal to ensure it it is not globally accepted/rejected @@ -3219,10 +3138,8 @@ fn reorg_locally_accepted_blocks_across_tenures_fails() { test_observer::clear(); // Start a new tenure and ensure the we see the expected rejections - signer_test - .running_nodes - .btc_regtest_controller - .build_next_block(1); + signer_test.mine_bitcoin_block(); + signer_test.wait_for_signer_state_update(); let proposal = wait_for_block_proposal_block(30, info_before.stacks_tip_height + 1, &miner_pk) .expect("Timed out waiting for block N+1 to be proposed"); wait_for_block_rejections_from_signers( @@ -3500,7 +3417,7 @@ fn bitcoin_reorg_extended_tenure() { .submit_burn_block_call_and_wait(&miners.sender_sk) .expect("Timed out waiting for contract-call"); - miners.submit_commit_miner_1(&sortdb); + miners.ensure_commit_miner_1(&sortdb); miners .mine_bitcoin_blocks_and_confirm(&sortdb, 1, 60) .unwrap(); @@ -3554,15 +3471,6 @@ fn reorging_signers_capitulate_to_nonreorging_signers_during_tenure_fork() { }, |_| {}, ); - let rl1_skip_commit_op = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - - let rl2_skip_commit_op = miners.rl2_counters.naka_skip_commit_op.clone(); - let (conf_1, _) = miners.get_node_configs(); let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes(); let (miner_pk_1, miner_pk_2) = miners.get_miner_public_keys(); @@ -3577,7 +3485,7 @@ fn reorging_signers_capitulate_to_nonreorging_signers_during_tenure_fork() { info!("------------------------- Pause Miner 2's Block Commits -------------------------"); // Make sure Miner 2 cannot win a sortition at first. - rl2_skip_commit_op.set(true); + miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); @@ -3591,9 +3499,8 @@ fn reorging_signers_capitulate_to_nonreorging_signers_during_tenure_fork() { ) .unwrap(); info!("------------------------- Pause Miner 1's Block Commit -------------------------"); - - // Make sure miner 1 doesn't submit any further block commits for the next tenure BEFORE mining the bitcoin block - rl1_skip_commit_op.set(true); + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); info!("------------------------- Miner 1 Wins Normal Tenure A -------------------------"); miners @@ -3615,7 +3522,7 @@ fn reorging_signers_capitulate_to_nonreorging_signers_during_tenure_fork() { TEST_BROADCAST_PROPOSAL_STALL.set(vec![miner_pk_1.clone()]); TEST_BLOCK_ANNOUNCE_STALL.set(true); - miners.submit_commit_miner_1(&sortdb); + miners.ensure_commit_miner_1(&sortdb); info!("------------------------- Miner 1 Wins Tenure B -------------------------"); miners @@ -3625,7 +3532,7 @@ fn reorging_signers_capitulate_to_nonreorging_signers_during_tenure_fork() { verify_sortition_winner(&sortdb, &miner_pkh_1); info!("----------------- Miner 2 Submits Block Commit for Tenure C Before Any Tenure B Blocks Produced ------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); let info = get_chain_info(&conf_1); info!("----------------------------- Resume Block Production for Tenure B -----------------------------"); @@ -3720,8 +3627,10 @@ fn reorging_signers_capitulate_to_nonreorging_signers_during_tenure_fork() { info!("--------------- Miner 1 Extends Tenure B over Tenure C ---------------"); TEST_BROADCAST_PROPOSAL_STALL.set(vec![]); let _tenure_extend_block = - wait_for_block_pushed_by_miner_key(30, tip_b.stacks_block_height + 1, &miner_pk_1) - .expect("Failed to mine miner 1's tenure extend block"); + wait_for_block_pushed_and_tip(30, tip_b.stacks_block_height + 1, &miner_pk_1, || { + miners.get_peer_info().stacks_tip + }) + .expect("Failed to mine miner 1's tenure extend block"); info!("------------------------- Miner 1 Mines Another Block -------------------------"); miners @@ -3729,7 +3638,7 @@ fn reorging_signers_capitulate_to_nonreorging_signers_during_tenure_fork() { .expect("Failed to mine tx"); info!("------------------------- Miner 2 Mines the Next Tenure -------------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); miners .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30) @@ -3813,14 +3722,6 @@ fn mark_miner_as_invalid_if_reorg_is_rejected_v1() { rejecting_signers.push(public_key); } } - let rl1_skip_commit_op = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - let rl2_skip_commit_op = miners.rl2_counters.naka_skip_commit_op.clone(); - let (conf_1, _) = miners.get_node_configs(); let (miner_pk_1, miner_pk_2) = miners.get_miner_public_keys(); let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes(); @@ -3828,7 +3729,7 @@ fn mark_miner_as_invalid_if_reorg_is_rejected_v1() { info!("------------------------- Pause Miner 2's Block Commits -------------------------"); // Make sure Miner 2 cannot win a sortition at first. - rl2_skip_commit_op.set(true); + miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); @@ -3836,7 +3737,8 @@ fn mark_miner_as_invalid_if_reorg_is_rejected_v1() { let sortdb = burnchain.open_sortition_db(true).unwrap(); info!("------------------------- Pause Miner 1's Block Commits -------------------------"); - rl1_skip_commit_op.set(true); + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); info!("------------------------- Miner 1 Mines a Nakamoto Block N -------------------------"); let info_before = get_chain_info(&conf_1); @@ -3850,21 +3752,15 @@ fn mark_miner_as_invalid_if_reorg_is_rejected_v1() { verify_sortition_winner(&sortdb, &miner_pkh_1); let block_n = - wait_for_block_pushed_by_miner_key(30, info_before.stacks_tip_height + 1, &miner_pk_1) - .expect("Failed to get block N"); + wait_for_block_pushed_and_tip(30, info_before.stacks_tip_height + 1, &miner_pk_1, || { + get_chain_info(&conf_1).stacks_tip + }) + .expect("Failed to get block N"); let block_n_height = block_n.header.chain_length; info!("Block N: {block_n_height}"); - let info_after = get_chain_info(&conf_1); - assert_eq!(info_after.stacks_tip, block_n.header.block_hash()); - assert_eq!( - info_after.stacks_tip_height, - info_before.stacks_tip_height + 1 - ); - assert_eq!(info_after.stacks_tip_height, block_n_height); - info!("------------------------- Miner 2 Submits a Block Commit -------------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); info!("------------------------- Pause Miner 2's Block Mining -------------------------"); fault_injection_stall_miner(); @@ -3875,17 +3771,15 @@ fn mark_miner_as_invalid_if_reorg_is_rejected_v1() { miners.signer_test.check_signer_states_normal(); info!("------------------------- Miner 1 Submits a Block Commit -------------------------"); - miners.submit_commit_miner_1(&sortdb); + miners.ensure_commit_miner_1(&sortdb); info!("------------------------- Miner 2 Mines Block N+1 -------------------------"); fault_injection_unstall_miner(); - let block_n_1 = wait_for_block_pushed_by_miner_key(30, block_n_height + 1, &miner_pk_2) - .expect("Failed to get block N+1"); - - let info_after = get_chain_info(&conf_1); - assert_eq!(info_after.stacks_tip_height, block_n_height + 1); - assert_eq!(info_after.stacks_tip, block_n_1.header.block_hash()); + let _block_n_1 = wait_for_block_pushed_and_tip(30, block_n_height + 1, &miner_pk_2, || { + get_chain_info(&conf_1).stacks_tip + }) + .expect("Failed to get block N+1"); // Wait for both chains to be in sync miners.wait_for_chains(30); @@ -3984,17 +3878,9 @@ fn miner_forking() { let (mining_pk_1, mining_pk_2) = miners.get_miner_public_keys(); let (mining_pkh_1, mining_pkh_2) = miners.get_miner_public_key_hashes(); - let skip_commit_op_rl1 = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - let skip_commit_op_rl2 = miners.rl2_counters.naka_skip_commit_op.clone(); - // Make sure that the first miner wins the first sortition. info!("Pausing miner 2's block commit submissions"); - skip_commit_op_rl2.set(true); + miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); let sortdb = conf_1.get_burnchain().open_sortition_db(true).unwrap(); @@ -4006,7 +3892,8 @@ fn miner_forking() { TEST_BROADCAST_PROPOSAL_STALL.set(vec![mining_pk_1.clone(), mining_pk_2.clone()]); info!("Pausing commits from RL1"); - skip_commit_op_rl1.set(true); + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); info!("Mine RL1 Tenure"); miners @@ -4022,7 +3909,7 @@ fn miner_forking() { info!( "------------------------- RL2 Wins Sortition With Outdated View -------------------------" ); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); // unblock block mining let blocks_len = test_observer::get_blocks().len(); @@ -4100,7 +3987,7 @@ fn miner_forking() { info!("------------------------- RL1 RBFs its Own Commit -------------------------"); info!("Pausing stacks block proposal to test RBF capability"); TEST_BROADCAST_PROPOSAL_STALL.set(vec![mining_pk_1.clone(), mining_pk_2.clone()]); - miners.submit_commit_miner_1(&sortdb); + miners.ensure_commit_miner_1(&sortdb); info!("Mine RL1 Tenure"); miners @@ -4109,7 +3996,7 @@ fn miner_forking() { miners .signer_test .check_signer_states_reorg(&miners.signer_test.signer_test_pks(), &[]); - miners.submit_commit_miner_1(&sortdb); + miners.ensure_commit_miner_1(&sortdb); // unblock block mining let blocks_len = test_observer::get_blocks().len(); TEST_BROADCAST_PROPOSAL_STALL.set(vec![]); @@ -4119,7 +4006,7 @@ fn miner_forking() { .expect("Timed out waiting for a block to be processed"); info!("Ensure that RL1 performs an RBF after unblocking block broadcast"); - miners.submit_commit_miner_1(&sortdb); + miners.ensure_commit_miner_1(&sortdb); info!("Mine RL1 Tenure"); miners @@ -4287,7 +4174,7 @@ fn revalidate_unknown_parent() { let rl2_coord_channels = run_loop_2.coordinator_channels(); let run_loop_stopper_2 = run_loop_2.get_termination_switch(); let Counters { - naka_skip_commit_op: rl2_skip_commit_op, + skip_commit_op: rl2_skip_commit_op, .. } = run_loop_2.counters(); let rl2_counters = run_loop_2.counters(); @@ -4317,11 +4204,7 @@ fn revalidate_unknown_parent() { info!("------------------------- Reached Epoch 3.0 -------------------------"); - let rl1_skip_commit_op = signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); + let rl1_skip_commit_op = signer_test.running_nodes.counters.skip_commit_op.clone(); let sortdb = SortitionDB::open( &conf.get_burn_db_file_path(), @@ -4618,17 +4501,16 @@ fn new_tenure_while_validating_previous_scenario() { info!("----- Mining BlockFound -----"); // Now, wait for miner B to propose a new block let block_pushed = - wait_for_block_pushed_by_miner_key(30, stacks_height_before_stall + 2, &miner_pk) - .expect("Timed out waiting for block N+2 to be mined"); + wait_for_block_pushed_and_tip(30, stacks_height_before_stall + 2, &miner_pk, || { + signer_test.get_peer_info().stacks_tip + }) + .expect("Timed out waiting for block N+2 to be mined"); // Ensure that we didn't tenure extend assert!(block_pushed .try_get_tenure_change_payload() .unwrap() .cause .is_eq(&TenureChangeCause::BlockFound)); - let peer_info = signer_test.get_peer_info(); - assert_eq!(peer_info.stacks_tip_height, stacks_height_before_stall + 2); - assert_eq!(peer_info.stacks_tip, block_pushed.header.block_hash()); info!("------------------------- Shutdown -------------------------"); signer_test.shutdown(); @@ -4662,16 +4544,15 @@ fn miner_rejection_by_contract_call_execution_time_expired() { signer_config.tenure_last_block_proposal_timeout = Duration::from_secs(1800); signer_config.first_proposal_burn_block_timing = Duration::from_secs(1800); }, - |config| config.miner.max_execution_time_secs = Some(0), - |config| config.miner.max_execution_time_secs = None, + |config| { + config.miner.max_execution_time_secs = Some(0); + config.miner.block_commit_delay = Duration::from_secs(0); + }, + |config| { + config.miner.max_execution_time_secs = None; + config.miner.block_commit_delay = Duration::from_secs(0); + }, ); - let rl1_skip_commit_op = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - let rl2_skip_commit_op = miners.rl2_counters.naka_skip_commit_op.clone(); let (conf_1, _) = miners.get_node_configs(); let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes(); @@ -4680,7 +4561,7 @@ fn miner_rejection_by_contract_call_execution_time_expired() { info!("------------------------- Pause Miner 2's Block Commits -------------------------"); // Make sure Miner 2 cannot win a sortition at first. - rl2_skip_commit_op.set(true); + miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); @@ -4688,7 +4569,8 @@ fn miner_rejection_by_contract_call_execution_time_expired() { let sortdb = burnchain.open_sortition_db(true).unwrap(); info!("------------------------- Pause Miner 1's Block Commits -------------------------"); - rl1_skip_commit_op.set(true); + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); info!("------------------------- Miner 1 Mines a Nakamoto Block N -------------------------"); miners @@ -4742,7 +4624,7 @@ fn miner_rejection_by_contract_call_execution_time_expired() { verify_sortition_winner(&sortdb, &miner_pkh_1); info!("------------------------- Miner 2 Submits a Block Commit -------------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); info!("------------------------- Mine Tenure -------------------------"); miners @@ -4802,16 +4684,15 @@ fn miner_rejection_by_contract_publish_execution_time_expired() { signer_config.tenure_last_block_proposal_timeout = Duration::from_secs(1800); signer_config.first_proposal_burn_block_timing = Duration::from_secs(1800); }, - |config| config.miner.max_execution_time_secs = Some(0), - |config| config.miner.max_execution_time_secs = None, + |config| { + config.miner.max_execution_time_secs = Some(0); + config.miner.block_commit_delay = Duration::from_secs(0); + }, + |config| { + config.miner.max_execution_time_secs = None; + config.miner.block_commit_delay = Duration::from_secs(0); + }, ); - let rl1_skip_commit_op = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - let rl2_skip_commit_op = miners.rl2_counters.naka_skip_commit_op.clone(); let (conf_1, _) = miners.get_node_configs(); let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes(); @@ -4820,7 +4701,7 @@ fn miner_rejection_by_contract_publish_execution_time_expired() { info!("------------------------- Pause Miner 2's Block Commits -------------------------"); // Make sure Miner 2 cannot win a sortition at first. - rl2_skip_commit_op.set(true); + miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); @@ -4828,7 +4709,9 @@ fn miner_rejection_by_contract_publish_execution_time_expired() { let sortdb = burnchain.open_sortition_db(true).unwrap(); info!("------------------------- Pause Miner 1's Block Commits -------------------------"); - rl1_skip_commit_op.set(true); + // Before pausing, make sure the miner is pointing to the right sortition DB state. + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); info!("------------------------- Miner 1 Mines a Nakamoto Block N -------------------------"); miners @@ -4852,7 +4735,7 @@ fn miner_rejection_by_contract_publish_execution_time_expired() { verify_sortition_winner(&sortdb, &miner_pkh_1); info!("------------------------- Miner 2 Submits a Block Commit -------------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); info!("------------------------- Mine Tenure -------------------------"); miners diff --git a/stacks-node/src/tests/signer/v0/reprocess_block_proposals.rs b/stacks-node/src/tests/signer/v0/reprocess_block_proposals.rs index cdee8731cfe..a8b03c10516 100644 --- a/stacks-node/src/tests/signer/v0/reprocess_block_proposals.rs +++ b/stacks-node/src/tests/signer/v0/reprocess_block_proposals.rs @@ -21,7 +21,7 @@ use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{fmt, EnvFilter}; use crate::tests::signer::v0::{ - MultipleMinerTest, wait_for_block_pre_commits_from_signers, wait_for_block_proposal, wait_for_block_pushed, wait_for_block_rejections_from_signers + MultipleMinerTest, wait_for_block_pre_commits_from_signers, wait_for_block_proposal, wait_for_block_pushed_by_miner_key, wait_for_block_rejections_from_signers }; #[test] @@ -96,20 +96,20 @@ fn signers_reprocess_bitcoin_block_not_found_proposals() { miners.boot_to_epoch_3(); // Make sure we know which miner will win in the stalled block - miners.pause_commits_miner_1(); - info!("------------------------- Mine First Block N -------------------------"); - let sortdb = SortitionDB::open( - &conf_1.get_burn_db_file_path(), - false, - conf_1.get_burnchain().pox_constants, + &conf_1.get_burn_db_file_path(), + false, + conf_1.get_burnchain().pox_constants, ) .unwrap(); + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); + info!("------------------------- Mine First Block N -------------------------"); // Mine an initial block to establish state miners .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30) .expect("Failed to mine BTC block followed by tenure change tx"); - miners.submit_commit_miner_1(&sortdb); + miners.ensure_commit_miner_1(&sortdb); miners.signer_test.check_signer_states_normal(); let info_before = miners.get_peer_info(); @@ -122,6 +122,7 @@ fn signers_reprocess_bitcoin_block_not_found_proposals() { info!("------------------------- Mine Block N+1 with Stalled Block Broadcasting -------------------------"); // Mine a new tenure which will issue a block proposal to all signers for its tenure change. miners.signer_test.mine_bitcoin_block(); + miners.signer_test.wait_for_signer_state_update(); // The 3 signers on miner 1 should have validated and sent pre-commits // The 2 signers on miner 2 should have issued a block rejection due to the stalled sortition commit preventing them from validating the block proposal @@ -142,7 +143,7 @@ fn signers_reprocess_bitcoin_block_not_found_proposals() { // Now that validation is resumed, the stalled signer should issue an approval wait_for_block_pre_commits_from_signers(30, &signer_signature_hash, &stalled_signers) .expect("Stalled signers failed to issue commits"); - wait_for_block_pushed(30, &signer_signature_hash).expect("Failed to mine block N+1"); + wait_for_block_pushed_by_miner_key(30, info_before.stacks_tip_height + 1, &miner_pk_1).expect("Failed to mine block N+1"); info!("------------------------- Shutting down -------------------------"); miners.shutdown(); } diff --git a/stacks-node/src/tests/signer/v0/signers_consider_consensus_blocks.rs b/stacks-node/src/tests/signer/v0/signers_consider_consensus_blocks.rs index 264fcd17b85..d1851067a5c 100644 --- a/stacks-node/src/tests/signer/v0/signers_consider_consensus_blocks.rs +++ b/stacks-node/src/tests/signer/v0/signers_consider_consensus_blocks.rs @@ -35,7 +35,7 @@ use crate::tests::nakamoto_integrations::wait_for; use crate::tests::neon_integrations::{submit_tx, test_observer}; use crate::tests::signer::v0::{ wait_for_block_acceptance_from_signers, wait_for_block_global_acceptance_from_signers, - wait_for_block_pre_commits_from_signers, wait_for_block_proposal, wait_for_block_pushed, + wait_for_block_pre_commits_from_signers, wait_for_block_proposal, wait_for_block_pushed_by_miner_key, wait_for_block_rejections_from_signers, MultipleMinerTest, }; use crate::tests::signer::SignerTest; @@ -119,10 +119,6 @@ fn signers_do_not_reconsider_globally_accepted_and_responded_blocks() { miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); - // Make sure we know which miner will win in the stalled block - miners.pause_commits_miner_1(); - info!("------------------------- Mine First Block N -------------------------"); - let sortdb = SortitionDB::open( &conf_1.get_burn_db_file_path(), false, @@ -130,11 +126,16 @@ fn signers_do_not_reconsider_globally_accepted_and_responded_blocks() { None, ) .unwrap(); + + // Make sure we know which miner will win in the stalled block + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); + info!("------------------------- Mine First Block N -------------------------"); // Mine an initial block to establish state miners .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30) .expect("Failed to mine BTC block followed by tenure change tx"); - miners.submit_commit_miner_1(&sortdb); + miners.ensure_commit_miner_1(&sortdb); miners.signer_test.check_signer_states_normal(); let info_before = miners.get_peer_info(); @@ -146,6 +147,7 @@ fn signers_do_not_reconsider_globally_accepted_and_responded_blocks() { info!("------------------------- Mine Block N+1 -------------------------"); // Mine a new tenure which will issue a block proposal to all signers for its tenure change. miners.signer_test.mine_bitcoin_block(); + miners.signer_test.wait_for_signer_state_update(); let block_proposal = wait_for_block_proposal(30, info_before.stacks_tip_height + 1, &miner_pk_1) @@ -153,7 +155,8 @@ fn signers_do_not_reconsider_globally_accepted_and_responded_blocks() { let signer_signature_hash = block_proposal.block.header.signer_signature_hash(); // The 4 signers on miner 1 should have validated and sent pre-commits // The 1 signer on miner 2 should immediately issue a block rejection. - wait_for_block_pushed(30, &signer_signature_hash).expect("Failed to mine block N+1"); + wait_for_block_pushed_by_miner_key(30, info_before.stacks_tip_height + 1, &miner_pk_1) + .expect("Failed to mine block N+1"); info!("------------------------- Check Signer Rejected Due to TestingDirective -------------------------"); let rejections = wait_for_block_rejections_from_signers(30, &signer_signature_hash, &rejecting_signer) @@ -247,17 +250,27 @@ fn signers_respond_to_unprocessed_globally_accepted_block_proposals() { miners.boot_to_epoch_3(); // Make sure we know which miner will win the tenure + let sortdb = SortitionDB::open( + &miners.get_node_configs().0.get_burn_db_file_path(), + false, + miners.get_node_configs().0.get_burnchain().pox_constants, + None, + ) + .unwrap(); + miners.ensure_commit_miner_1(&sortdb); miners.pause_commits_miner_1(); TEST_SIGNERS_INSERT_BLOCK_PROPOSAL_WITHOUT_PROCESSING.set(nonprocessing_signers.clone()); info!("------------------------- Mine Tenure A and Propose Block N -------------------------"); let expected_height = miners.signer_test.get_peer_info().stacks_tip_height + 1; miners.signer_test.mine_bitcoin_block(); + miners.signer_test.wait_for_signer_state_update(); info!("------------------------- Wait for block proposal -------------------------"); let block_proposal = wait_for_block_proposal(30, expected_height, &miner_pk_1) .expect("Miner failed to propose tenure start block"); let sighash = block_proposal.block.header.signer_signature_hash(); - wait_for_block_pushed(30, &sighash).expect("Block proposal was not globally accepted"); + wait_for_block_pushed_by_miner_key(30, expected_height, &miner_pk_1) + .expect("Block proposal was not globally accepted"); info!( "------------------------- Wait for block pre-commits/signatures -------------------------" ); diff --git a/stacks-node/src/tests/signer/v0/signers_consider_late_proposals.rs b/stacks-node/src/tests/signer/v0/signers_consider_late_proposals.rs index 0aafecc4e53..adb32b0c307 100644 --- a/stacks-node/src/tests/signer/v0/signers_consider_late_proposals.rs +++ b/stacks-node/src/tests/signer/v0/signers_consider_late_proposals.rs @@ -26,10 +26,9 @@ use tracing_subscriber::{fmt, EnvFilter}; use super::SignerTest; use crate::nakamoto_node::stackerdb_listener::TEST_IGNORE_SIGNERS; -use crate::tests::nakamoto_integrations::wait_for; use crate::tests::signer::v0::{ wait_for_block_acceptance_from_signers, wait_for_block_pre_commits_from_signers, - wait_for_block_proposal, wait_for_block_pushed, + wait_for_block_proposal, wait_for_block_pushed_and_tip, }; #[test] @@ -87,6 +86,7 @@ fn signers_reprocess_late_block_proposals_pre_commits() { info!("------------------------- Mine Tenure A and Propose Block N -------------------------"); let expected_height = signer_test.get_peer_info().stacks_tip_height + 1; signer_test.mine_bitcoin_block(); + signer_test.wait_for_signer_state_update(); info!("------------------------- Wait for block proposal -------------------------"); let block_proposal = wait_for_block_proposal(30, expected_height, &miner_pk) .expect("Miner failed to propose tenure start block"); @@ -109,12 +109,10 @@ fn signers_reprocess_late_block_proposals_pre_commits() { wait_for_block_acceptance_from_signers(30, &sighash, &all_signers) .expect("All signers should have accepted the block proposal after it was reproposed"); info!("------------------------- Wait for block pushed -------------------------"); - wait_for_block_pushed(30, &sighash) - .expect("Block should have been pushed to the node after being accepted by all signers"); - wait_for(30, || { - Ok(signer_test.get_peer_info().stacks_tip_height == expected_height) + wait_for_block_pushed_and_tip(30, expected_height, &miner_pk, || { + signer_test.get_peer_info().stacks_tip }) - .expect("Node should have advanced to expected height after block acceptance"); + .expect("Block should have been pushed to the node after being accepted by all signers"); } #[test] @@ -173,6 +171,7 @@ fn signers_reprocess_late_block_proposals_signatures() { info!("------------------------- Mine Tenure A and Propose Block N -------------------------"); let expected_height = signer_test.get_peer_info().stacks_tip_height + 1; signer_test.mine_bitcoin_block(); + signer_test.wait_for_signer_state_update(); info!("------------------------- Wait for block proposal -------------------------"); let block_proposal = wait_for_block_proposal(30, expected_height, &miner_pk) .expect("Miner failed to propose tenure start block"); @@ -207,10 +206,8 @@ fn signers_reprocess_late_block_proposals_signatures() { wait_for_block_acceptance_from_signers(30, &sighash, &ignoring_signers) .expect("Ignoring signer should have accepted the block proposal after it was reproposed"); info!("------------------------- Wait for block pushed -------------------------"); - wait_for_block_pushed(30, &sighash) - .expect("Block should have been pushed to the node after the threshold was exceeded by the late signer"); - wait_for(30, || { - Ok(signer_test.get_peer_info().stacks_tip_height == expected_height) + wait_for_block_pushed_and_tip(30, expected_height, &miner_pk, || { + signer_test.get_peer_info().stacks_tip }) - .expect("Node should have advanced to expected height after block acceptance"); + .expect("Block should have been pushed to the node after the threshold was exceeded by the late signer"); } diff --git a/stacks-node/src/tests/signer/v0/signers_wait_for_validation.rs b/stacks-node/src/tests/signer/v0/signers_wait_for_validation.rs index 2c815f5b7d4..f017749f3ba 100644 --- a/stacks-node/src/tests/signer/v0/signers_wait_for_validation.rs +++ b/stacks-node/src/tests/signer/v0/signers_wait_for_validation.rs @@ -17,16 +17,15 @@ use std::env; use libsigner::v0::messages::{BlockResponse, SignerMessage}; use stacks::chainstate::burn::db::sortdb::SortitionDB; use stacks::chainstate::stacks::TenureChangeCause; -use stacks::codec::StacksMessageCodec; use stacks::net::api::postblock_proposal::TEST_VALIDATE_STALL; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{fmt, EnvFilter}; use crate::tests::nakamoto_integrations::wait_for; -use crate::tests::neon_integrations::test_observer; use crate::tests::signer::v0::{ - wait_for_block_pre_commits_from_signers, wait_for_block_pushed_by_miner_key, MultipleMinerTest, + get_stackerdb_signer_messages, wait_for_block_pre_commits_from_signers, + wait_for_block_pushed_by_miner_key, MultipleMinerTest, }; #[test] @@ -112,10 +111,6 @@ fn signer_waits_for_validation_before_signing() { miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); - // Make sure we know which miner will win in the stalled block - miners.pause_commits_miner_1(); - info!("------------------------- Mine First Block N -------------------------"); - let sortdb = SortitionDB::open( &conf_1.get_burn_db_file_path(), false, @@ -123,11 +118,15 @@ fn signer_waits_for_validation_before_signing() { None, ) .unwrap(); + // Make sure we know which miner will win in the stalled block + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); + info!("------------------------- Mine First Block N -------------------------"); // Mine an initial block to establish state miners .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30) .expect("Failed to mine BTC block followed by tenure change tx"); - miners.submit_commit_miner_1(&sortdb); + miners.ensure_commit_miner_1(&sortdb); miners.signer_test.check_signer_states_normal(); let info_before = miners.get_peer_info(); @@ -153,13 +152,7 @@ fn signer_waits_for_validation_before_signing() { let stalled_pk = stalled_signer[0].clone(); assert!( wait_for(15, || { - for chunk in test_observer::get_stackerdb_chunks() - .into_iter() - .flat_map(|chunk| chunk.modified_slots) - { - let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - .expect("Failed to deserialize SignerMessage"); - + for (chunk, message) in get_stackerdb_signer_messages() { let pk = chunk.recover_pk().expect("Failed to recover pk"); if stalled_pk != pk { continue; @@ -202,13 +195,7 @@ fn signer_waits_for_validation_before_signing() { let mut found_commit = false; let mut found_accept = false; wait_for(15, || { - for chunk in test_observer::get_stackerdb_chunks() - .into_iter() - .flat_map(|chunk| chunk.modified_slots) - { - let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - .expect("Failed to deserialize SignerMessage"); - + for (chunk, message) in get_stackerdb_signer_messages() { let pk = chunk.recover_pk().expect("Failed to recover pk"); if stalled_pk != pk { continue; diff --git a/stacks-node/src/tests/signer/v0/tenure_extend.rs b/stacks-node/src/tests/signer/v0/tenure_extend.rs index 1d0c6954ba8..aec86665cab 100644 --- a/stacks-node/src/tests/signer/v0/tenure_extend.rs +++ b/stacks-node/src/tests/signer/v0/tenure_extend.rs @@ -329,21 +329,15 @@ fn tenure_extend_after_idle_signers_with_buffer() { // Check the tenure extend timestamps to verify that they have factored in the buffer let blocks = test_observer::get_mined_nakamoto_blocks(); let last_block = blocks.last().expect("No blocks mined"); - let timestamps: HashSet<_> = test_observer::get_stackerdb_chunks() + let timestamps: HashSet<_> = get_stackerdb_signer_messages() .into_iter() - .flat_map(|chunk| chunk.modified_slots) - .filter_map(|chunk| { - let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - .expect("Failed to deserialize SignerMessage"); - - match message { - SignerMessage::BlockResponse(BlockResponse::Accepted(accepted)) - if accepted.signer_signature_hash == last_block.signer_signature_hash => - { - Some(accepted.response_data.tenure_extend_timestamp) - } - _ => None, + .filter_map(|(_chunk, message)| match message { + SignerMessage::BlockResponse(BlockResponse::Accepted(accepted)) + if accepted.signer_signature_hash == last_block.signer_signature_hash => + { + Some(accepted.response_data.tenure_extend_timestamp) } + _ => None, }) .collect(); for timestamp in timestamps { @@ -1222,15 +1216,15 @@ fn tenure_extend_after_stale_commit_different_miner() { miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); - miners.pause_commits_miner_1(); - let sortdb = conf_1.get_burnchain().open_sortition_db(true).unwrap(); + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); miners .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 60) .unwrap(); - miners.submit_commit_miner_1(&sortdb); + miners.ensure_commit_miner_1(&sortdb); info!("------------------------- Miner 1 Wins Tenure A -------------------------"); miners @@ -1242,7 +1236,7 @@ fn tenure_extend_after_stale_commit_different_miner() { let prev_tip = get_chain_info(&conf_1); info!("------------------------- Miner 2 Wins Tenure B -------------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); miners .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 60) .unwrap(); @@ -1253,7 +1247,7 @@ fn tenure_extend_after_stale_commit_different_miner() { info!("------------------------- Miner 1 Wins Tenure C with stale commit -------------------------"); - // We can't use `submit_commit_miner_1` here because we are using the stale view + // We can't use `ensure_commit_miner_1` here because we are using the stale view { TEST_MINER_COMMIT_TIP.set(Some((prev_tip.pox_consensus, prev_tip.stacks_tip))); let rl1_commits_before = miners @@ -1267,7 +1261,7 @@ fn tenure_extend_after_stale_commit_different_miner() { .signer_test .running_nodes .counters - .naka_skip_commit_op + .skip_commit_op .set(false); wait_for(30, || { @@ -1292,7 +1286,7 @@ fn tenure_extend_after_stale_commit_different_miner() { .signer_test .running_nodes .counters - .naka_skip_commit_op + .skip_commit_op .set(true); TEST_MINER_COMMIT_TIP.set(None); } @@ -1393,7 +1387,7 @@ fn tenure_extend_after_stale_commit_same_miner() { ); let Counters { - naka_skip_commit_op: skip_commit_op, + skip_commit_op, naka_submitted_commit_last_burn_height: last_commit_burn_height, .. } = signer_test.running_nodes.counters.clone(); @@ -1440,6 +1434,7 @@ fn tenure_extend_after_stale_commit_same_miner() { let stacks_height_before = info_before.stacks_tip_height; signer_test.mine_bitcoin_block(); + signer_test.wait_for_signer_state_update(); verify_sortition_winner(&sortdb, &miner_pkh); @@ -1526,7 +1521,7 @@ fn tenure_extend_after_stale_commit_same_miner_then_no_winner() { ); let Counters { - naka_skip_commit_op: skip_commit_op, + skip_commit_op, naka_submitted_commit_last_burn_height: last_commit_burn_height, .. } = signer_test.running_nodes.counters.clone(); @@ -1576,6 +1571,7 @@ fn tenure_extend_after_stale_commit_same_miner_then_no_winner() { skip_commit_op.set(true); signer_test.mine_bitcoin_block(); + signer_test.wait_for_signer_state_update(); verify_sortition_winner(&sortdb, &miner_pkh); @@ -1601,6 +1597,7 @@ fn tenure_extend_after_stale_commit_same_miner_then_no_winner() { // Now, mine the next bitcoin block, which should have no winner signer_test.mine_bitcoin_block(); + signer_test.wait_for_signer_state_update(); info!("---- Waiting for a tenure extend block in tenure N+2 ----"; "stacks_height_before" => stacks_height_before, @@ -1763,27 +1760,18 @@ fn tenure_extend_after_failed_miner() { let (conf_1, _) = miners.get_node_configs(); let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes(); - let rl1_skip_commit_op = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - let rl2_skip_commit_op = miners.rl2_counters.naka_skip_commit_op.clone(); - info!("------------------------- Pause Miner 2's Block Commits -------------------------"); // Make sure Miner 2 cannot win a sortition at first. - rl2_skip_commit_op.set(true); + miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); let burnchain = conf_1.get_burnchain(); let sortdb = burnchain.open_sortition_db(true).unwrap(); - info!("------------------------- Pause Miner 1's Block Commit -------------------------"); - // Make sure miner 1 doesn't submit any further block commits for the next tenure BEFORE mining the bitcoin block - rl1_skip_commit_op.set(true); + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); let starting_peer_height = get_chain_info(&conf_1).stacks_tip_height; info!("------------------------- Miner 1 Wins Normal Tenure A -------------------------"); @@ -1802,7 +1790,7 @@ fn tenure_extend_after_failed_miner() { info!("------------------------- Pause Block Proposals -------------------------"); fault_injection_stall_miner(); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); info!("------------------------- Miner 2 Wins Tenure B, Mines No Blocks -------------------------"); let stacks_height_before = miners.get_peer_stacks_tip_height(); @@ -1881,32 +1869,21 @@ fn tenure_extend_after_bad_commit() { config.miner.block_commit_delay = Duration::from_secs(0); }, ); - let rl1_skip_commit_op = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - - let rl2_skip_commit_op = miners.rl2_counters.naka_skip_commit_op.clone(); - let (conf_1, _) = miners.get_node_configs(); let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes(); info!("------------------------- Pause Miner 2's Block Commits -------------------------"); // Make sure Miner 2 cannot win a sortition at first. - rl2_skip_commit_op.set(true); + miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); let burnchain = conf_1.get_burnchain(); let sortdb = burnchain.open_sortition_db(true).unwrap(); - info!("------------------------- Pause Miner 1's Block Commit -------------------------"); - - // Make sure miner 1 doesn't submit any further block commits for the next tenure BEFORE mining the bitcoin block - rl1_skip_commit_op.set(true); + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); info!("------------------------- Miner 1 Wins Normal Tenure A -------------------------"); miners @@ -1922,7 +1899,7 @@ fn tenure_extend_after_bad_commit() { info!("------------------------- Pause Block Proposals -------------------------"); fault_injection_stall_miner(); - miners.submit_commit_miner_1(&sortdb); + miners.ensure_commit_miner_1(&sortdb); info!("------------------------- Miner 1 Wins Tenure B -------------------------"); miners @@ -1932,7 +1909,7 @@ fn tenure_extend_after_bad_commit() { verify_sortition_winner(&sortdb, &miner_pkh_1); info!("----------------- Miner 2 Submits Block Commit Before Any Blocks ------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); info!("----------------------------- Resume Block Production -----------------------------"); @@ -1965,7 +1942,7 @@ fn tenure_extend_after_bad_commit() { .expect("Failed to mine tx"); info!("------------------------- Miner 2 Mines the Next Tenure -------------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); miners .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30) @@ -2003,23 +1980,19 @@ fn tenure_extend_after_2_bad_commits() { |signer_config| { signer_config.block_proposal_timeout = block_proposal_timeout; }, - |_| {}, - |_| {}, + |config| { + config.miner.block_commit_delay = Duration::from_secs(0); + }, + |config| { + config.miner.block_commit_delay = Duration::from_secs(0); + }, ); - let rl1_skip_commit_op = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - let rl2_skip_commit_op = miners.rl2_counters.naka_skip_commit_op.clone(); - let (conf_1, _) = miners.get_node_configs(); let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes(); info!("------------------------- Pause Miner 2's Block Commits -------------------------"); // Make sure Miner 2 cannot win a sortition at first. - rl2_skip_commit_op.set(true); + miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); @@ -2027,8 +2000,8 @@ fn tenure_extend_after_2_bad_commits() { let sortdb = burnchain.open_sortition_db(true).unwrap(); info!("------------------------- Pause Miner 1's Block Commit -------------------------"); - // Make sure miner 1 doesn't submit any further block commits for the next tenure BEFORE mining the bitcoin block - rl1_skip_commit_op.set(true); + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); info!("------------------------- Miner 1 Wins Normal Tenure A -------------------------"); miners @@ -2046,7 +2019,7 @@ fn tenure_extend_after_2_bad_commits() { info!("------------------------- Pause Block Proposals -------------------------"); fault_injection_stall_miner(); - miners.submit_commit_miner_1(&sortdb); + miners.ensure_commit_miner_1(&sortdb); info!("------------------------- Miner 1 Wins Tenure B -------------------------"); miners @@ -2057,7 +2030,7 @@ fn tenure_extend_after_2_bad_commits() { verify_sortition_winner(&sortdb, &miner_pkh_1); info!("----------------- Miner 2 Submits Block Commit Before Any Blocks ------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); info!("----------------------------- Resume Block Production -----------------------------"); @@ -2078,7 +2051,7 @@ fn tenure_extend_after_2_bad_commits() { verify_sortition_winner(&sortdb, &miner_pkh_2); info!("---------- Miner 2 Submits Block Commit Before Any Blocks (again) ----------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); info!("------------------------- Miner 1 Extends Tenure B -------------------------"); @@ -2102,7 +2075,7 @@ fn tenure_extend_after_2_bad_commits() { // assure we have a successful sortition that miner 2 won verify_sortition_winner(&sortdb, &miner_pkh_2); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); info!("---------------------- Miner 1 Extends Tenure B (again) ---------------------"); @@ -2119,7 +2092,7 @@ fn tenure_extend_after_2_bad_commits() { .expect("Failed to mine tx"); info!("----------------------- Miner 2 Mines the Next Tenure -----------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); miners .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30) @@ -2174,14 +2147,6 @@ fn prev_miner_extends_if_incoming_miner_fails_to_mine_success() { }, ); - let rl1_skip_commit_op = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - let rl2_skip_commit_op = miners.rl2_counters.naka_skip_commit_op.clone(); - let (conf_1, _) = miners.get_node_configs(); let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes(); let (miner_pk_1, miner_pk_2) = miners.get_miner_public_keys(); @@ -2189,7 +2154,7 @@ fn prev_miner_extends_if_incoming_miner_fails_to_mine_success() { info!("------------------------- Pause Miner 2's Block Commits -------------------------"); // Make sure Miner 2 cannot win a sortition at first. - rl2_skip_commit_op.set(true); + miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); @@ -2206,8 +2171,8 @@ fn prev_miner_extends_if_incoming_miner_fails_to_mine_success() { let mut btc_blocks_mined = 0; info!("------------------------- Pause Miner 1's Block Commit -------------------------"); - // Make sure miner 1 doesn't submit any further block commits for the next tenure BEFORE mining the bitcoin block - rl1_skip_commit_op.set(true); + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); info!("------------------------- Miner 1 Mines a Normal Tenure A -------------------------"); miners @@ -2218,7 +2183,7 @@ fn prev_miner_extends_if_incoming_miner_fails_to_mine_success() { verify_sortition_winner(&sortdb, &miner_pkh_1); info!("------------------------- Submit Miner 2 Block Commit -------------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); // Pause the block proposal broadcast so that miner 2 will be unable to broadcast its // tenure change proposal BEFORE the block_proposal_timeout and will be marked invalid. @@ -2254,9 +2219,11 @@ fn prev_miner_extends_if_incoming_miner_fails_to_mine_success() { info!("------------------------- Wait for Miner 1's Block N+1 to be Mined ------------------------"; "stacks_height_before" => %stacks_height_before); - let miner_1_block_n_1 = - wait_for_block_pushed_by_miner_key(30, stacks_height_before + 1, &miner_pk_1) - .expect("Timed out waiting for block proposal N+1 from miner 1"); + let _miner_1_block_n_1 = + wait_for_block_pushed_and_tip(30, stacks_height_before + 1, &miner_pk_1, || { + miners.get_peer_info().stacks_tip + }) + .expect("Timed out waiting for block proposal N+1 from miner 1"); let miner_2_block_n_1 = wait_for_block_proposal_block(30, stacks_height_before + 1, &miner_pk_2) @@ -2269,17 +2236,13 @@ fn prev_miner_extends_if_incoming_miner_fails_to_mine_success() { ) .expect("Timed out waiting for global rejection of Miner 2's block N+1'"); - let peer_info = miners.get_peer_info(); - assert_eq!(peer_info.stacks_tip, miner_1_block_n_1.header.block_hash()); - assert_eq!(peer_info.stacks_tip_height, stacks_height_before + 1); - info!( "------------------------- Verify Tenure Change Extend Tx in Miner 1's Block N+1 -------------------------" ); verify_last_block_contains_tenure_change_tx(TenureChangeCause::Extended); info!("------------------------- Unpause Miner 2's Block Commits -------------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); info!("------------------------- Miner 2 Mines a Normal Tenure C -------------------------"); @@ -2355,18 +2318,10 @@ fn prev_miner_extends_if_incoming_miner_fails_to_mine_failure() { let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes(); let (miner_pk_1, miner_pk_2) = miners.get_miner_public_keys(); - let rl1_skip_commit_op = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - let rl2_skip_commit_op = miners.rl2_counters.naka_skip_commit_op.clone(); - info!("------------------------- Pause Miner 2's Block Commits -------------------------"); // Make sure Miner 2 cannot win a sortition at first. - rl2_skip_commit_op.set(true); + miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); @@ -2383,8 +2338,8 @@ fn prev_miner_extends_if_incoming_miner_fails_to_mine_failure() { let mut btc_blocks_mined = 0; info!("------------------------- Pause Miner 1's Block Commit -------------------------"); - // Make sure miner 1 doesn't submit any further block commits for the next tenure BEFORE mining the bitcoin block - rl1_skip_commit_op.set(true); + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); info!("------------------------- Miner 1 Mines a Normal Tenure A -------------------------"); miners @@ -2397,7 +2352,7 @@ fn prev_miner_extends_if_incoming_miner_fails_to_mine_failure() { info!("------------------------- Submit Miner 2 Block Commit -------------------------"); let stacks_height_before = miners.get_peer_stacks_tip_height(); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); let burn_height_before = get_burn_height(); @@ -2454,23 +2409,18 @@ fn prev_miner_extends_if_incoming_miner_fails_to_mine_failure() { TEST_BROADCAST_PROPOSAL_STALL.set(vec![]); - // Get miner 2's N+1 block proposal - let miner_2_block_n_1 = - wait_for_block_proposal_block(30, stacks_height_before + 1, &miner_pk_2) - .expect("Timed out waiting for N+1 block proposal from miner 2"); - info!("------------------------- Wait for Miner 2's Block N+1 to be Approved ------------------------"; "stacks_height_before" => %stacks_height_before ); - // Miner 2's proposed block should get approved and pushed - let miner_2_block_n_1 = - wait_for_block_pushed(30, &miner_2_block_n_1.header.signer_signature_hash()) - .expect("Timed out waiting for Block N+1 to be pushed"); - - let peer_info = miners.get_peer_info(); - assert_eq!(peer_info.stacks_tip, miner_2_block_n_1.header.block_hash()); - assert_eq!(peer_info.stacks_tip_height, stacks_height_before + 1); + // Miner 2's proposed block should get approved and pushed. + // Use wait_for_block_pushed_by_miner_key to avoid matching a stale proposal + // (there may be multiple proposals for the same height). + let _miner_2_block_n_1 = + wait_for_block_pushed_and_tip(60, stacks_height_before + 1, &miner_pk_2, || { + miners.get_peer_info().stacks_tip + }) + .expect("Timed out waiting for Block N+1 to be pushed"); info!( "------------------------- Verify BlockFound in Miner 2's Block N+1 -------------------------" @@ -2478,7 +2428,7 @@ fn prev_miner_extends_if_incoming_miner_fails_to_mine_failure() { verify_last_block_contains_tenure_change_tx(TenureChangeCause::BlockFound); info!("------------------------- Unpause Miner 2's Block Commits -------------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); info!("------------------------- Miner 2 Mines a Normal Tenure C -------------------------"); @@ -2555,18 +2505,10 @@ fn prev_miner_will_not_attempt_to_extend_if_incoming_miner_produces_a_block() { let (miner_pk_1, miner_pk_2) = miners.get_miner_public_keys(); let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes(); - let rl1_skip_commit_op = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - let rl2_skip_commit_op = miners.rl2_counters.naka_skip_commit_op.clone(); - info!("------------------------- Pause Miner 2's Block Commits -------------------------"); // Make sure Miner 2 cannot win a sortition at first. - rl2_skip_commit_op.set(true); + miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); @@ -2583,8 +2525,8 @@ fn prev_miner_will_not_attempt_to_extend_if_incoming_miner_produces_a_block() { let mut btc_blocks_mined = 0; info!("------------------------- Pause Miner 1's Block Commit -------------------------"); - // Make sure miner 1 doesn't submit any further block commits for the next tenure BEFORE mining the bitcoin block - rl1_skip_commit_op.set(true); + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); info!("------------------------- Miner 1 Mines a Normal Tenure A -------------------------"); miners @@ -2597,7 +2539,7 @@ fn prev_miner_will_not_attempt_to_extend_if_incoming_miner_produces_a_block() { info!("------------------------- Submit Miner 2 Block Commit -------------------------"); let stacks_height_before = miners.get_peer_stacks_tip_height(); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); let burn_height_before = get_burn_height(); @@ -2615,16 +2557,12 @@ fn prev_miner_will_not_attempt_to_extend_if_incoming_miner_produces_a_block() { info!("------------------------- Get Miner 2's N+1 block -------------------------"); - let miner_2_block_n_1 = - wait_for_block_proposal_block(60, stacks_height_before + 1, &miner_pk_2) - .expect("Timed out waiting for N+1 block proposal from miner 2"); - let miner_2_block_n_1 = - wait_for_block_pushed(30, &miner_2_block_n_1.header.signer_signature_hash()) - .expect("Timed out waiting for N+1 block to be approved"); - + let _miner_2_block_n_1 = + wait_for_block_pushed_and_tip(60, stacks_height_before + 1, &miner_pk_2, || { + miners.get_peer_info().stacks_tip + }) + .expect("Timed out waiting for N+1 block to be approved"); let peer_info = miners.get_peer_info(); - assert_eq!(peer_info.stacks_tip, miner_2_block_n_1.header.block_hash()); - assert_eq!(peer_info.stacks_tip_height, stacks_height_before + 1); let stacks_height_before = peer_info.stacks_tip_height; @@ -2691,11 +2629,7 @@ fn continue_after_tenure_extend() { signer_test.mine_and_verify_confirmed_naka_block(timeout, num_signers, true); info!("------------------------- Pause Block Commits-------------------------"); - signer_test - .running_nodes - .counters - .naka_skip_commit_op - .set(true); + signer_test.running_nodes.counters.skip_commit_op.set(true); info!("------------------------- Flush Pending Commits -------------------------"); // Mine a couple blocks to flush the last submitted commit out. let peer_info = signer_test.get_peer_info(); @@ -2775,10 +2709,7 @@ fn burn_block_height_behavior() { signer_test.boot_to_epoch_3(); - let Counters { - naka_skip_commit_op: skip_commit_op, - .. - } = signer_test.running_nodes.counters.clone(); + let Counters { skip_commit_op, .. } = signer_test.running_nodes.counters.clone(); info!("------------------------- Test Mine Regular Tenure A -------------------------"); @@ -3004,7 +2935,7 @@ fn new_tenure_no_winner_while_proposing_block() { ); let Counters { - naka_skip_commit_op: skip_commit_op, + skip_commit_op, naka_submitted_commit_last_burn_height: last_commit_burn_height, .. } = signer_test.running_nodes.counters.clone(); @@ -3161,7 +3092,7 @@ fn new_tenure_no_winner_while_proposing_block_then_rejected() { ); let Counters { - naka_skip_commit_op: skip_commit_op, + skip_commit_op, naka_submitted_commit_last_burn_height: last_commit_burn_height, .. } = signer_test.running_nodes.counters.clone(); @@ -3346,7 +3277,7 @@ fn new_tenure_no_winner_while_proposing_block_then_ignored() { ); let Counters { - naka_skip_commit_op: skip_commit_op, + skip_commit_op, naka_submitted_commit_last_burn_height: last_commit_burn_height, .. } = signer_test.running_nodes.counters.clone(); @@ -3454,62 +3385,63 @@ fn new_tenure_no_winner_while_proposing_block_then_ignored() { signer_test.shutdown(); } -/// Test a scenario where a non-blocking minority of signers are configured to favour the incoming miner. -/// The previous miner should extend its tenure and succeed as a majority are configured to favour it -/// and its subsequent blocks should be be approved. -/// Two miners boot to Nakamoto. -/// Miner 1 wins the first tenure A. -/// Miner 1 proposes a block N with a TenureChangeCause::BlockFound -/// Signers accept and the stacks tip advances to N -/// Miner 2 wins the second tenure B. -/// A majority of signers mark miner 2 as invalid. -/// Miner 2 proposes block N+1' with a TenureChangeCause::BlockFound -/// A majority fo signers reject block N+1'. -/// Miner 1 proposes block N+1 with a TenureChangeCause::Extended -/// A majority of signers accept and the stacks tip advances to N+1 -/// Miner 1 proposes block N+2 with a transfer tx -/// ALL signers should accept block N+2. -/// Miner 2 wins the third tenure C. -/// Miner 2 proposes block N+3 with a TenureChangeCause::BlockFound -/// Signers accept and the stacks tip advances to N+3 +/// Describes the variant of the non-blocking minority test. /// -/// Asserts: -/// - Block N contains the TenureChangeCause::BlockFound -/// - Block N+1' contains a TenureChangeCause::BlockFound and is rejected -/// - Block N+1 contains the TenureChangeCause::Extended -/// - Block N+2 is accepted. -/// - Block N+3 contains the TenureChangeCause::BlockFound. -/// - The stacks tip advances to N+3 -#[test] -#[ignore] -fn non_blocking_minority_configured_to_favour_incoming_miner() { +/// All variants share the same structure: +/// 1. Two miners boot to Nakamoto +/// 2. Miner 1 wins Tenure A +/// 3. Miner 2 wins Tenure B, but proposals are stalled +/// 4. After the block_proposal_timeout, one miner's block is rejected +/// 5. The other miner's block is accepted +/// 6. A transfer tx is mined in block N+2 +/// 7. Tenure C is mined +/// 8. Final height assertions +enum NonBlockingMinorityVariant { + /// Minority favours incoming miner (long timeout), majority has short timeout. + /// Majority marks incoming miner invalid → incoming miner (miner 2) rejected → + /// prev miner (miner 1) extends successfully. + FavourIncomingMiner, + /// Minority favours prev miner (short timeout), majority has long timeout. + /// Prev miner (miner 1) extend rejected by majority → incoming miner (miner 2) succeeds. + /// Signers pinned to protocol v1. + FavourPrevMinerV1, + /// Same as FavourPrevMinerV1 but with the latest signer protocol version. + /// ALL signers reject the extend and ALL accept the incoming miner's block. + FavourPrevMiner, +} + +/// Shared implementation for the non_blocking_minority_configured_to_favour_* tests. +fn non_blocking_minority_configured_to_favour_test(variant: NonBlockingMinorityVariant) { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; } let num_signers = 5; - let num_txs = 1; let non_block_minority = num_signers * 2 / 10; + let num_txs = 1; + + let short_timeout = Duration::from_secs(20); + let long_timeout = Duration::from_secs(500); + let tenure_extend_wait_timeout = short_timeout; - let favour_prev_miner_block_proposal_timeout = Duration::from_secs(20); - let favour_incoming_miner_block_proposal_timeout = Duration::from_secs(500); - // Make sure the miner attempts to extend after the minority mark the incoming as invalid - let tenure_extend_wait_timeout = favour_prev_miner_block_proposal_timeout; + let minority_favours_incoming = + matches!(variant, NonBlockingMinorityVariant::FavourIncomingMiner); info!("------------------------- Test Setup -------------------------"); - // partition the signer set so that ~half are listening and using node 1 for RPC and events, - // and the rest are using node 2 let mut miners = MultipleMinerTest::new_with_config_modifications( num_signers, num_txs, |signer_config| { let port = signer_config.endpoint.port(); - // Note signer ports are based on the number of them, the first being 3000, the last being 3000 + num_signers - 1 - if port < 3000 + non_block_minority as u16 { - signer_config.block_proposal_timeout = favour_incoming_miner_block_proposal_timeout; + let is_minority = port < 3000 + non_block_minority as u16; + // Minority signers that favour incoming get a long timeout (tolerant of incoming), + // while minority signers that favour prev get a short timeout (will mark incoming invalid). + // The majority gets the opposite timeout. + signer_config.block_proposal_timeout = if is_minority == minority_favours_incoming { + long_timeout } else { - signer_config.block_proposal_timeout = favour_prev_miner_block_proposal_timeout; - } + short_timeout + }; }, |config| { config.miner.tenure_extend_wait_timeout = tenure_extend_wait_timeout; @@ -3520,22 +3452,39 @@ fn non_blocking_minority_configured_to_favour_incoming_miner() { }, ); + let all_signers = miners.signer_test.signer_test_pks(); + + // Compute the set of signers with the short timeout (those that will mark incoming invalid). + let short_timeout_keys = if minority_favours_incoming { + &all_signers[non_block_minority..] // majority has short timeout + } else { + &all_signers[..non_block_minority] // minority has short timeout + }; + + // Pin signers to v1 protocol BEFORE computing short_timeout_signers, + // so that signer_addresses_versions() returns the correct pinned version. + if matches!(variant, NonBlockingMinorityVariant::FavourPrevMinerV1) { + let pinned_signers = all_signers.iter().map(|key| (key.clone(), 1)).collect(); + TEST_PIN_SUPPORTED_SIGNER_PROTOCOL_VERSION.set(pinned_signers); + } + + let short_timeout_signers: Vec<_> = miners + .signer_test + .signer_addresses_versions() + .into_iter() + .filter(|(address, _)| { + short_timeout_keys + .iter() + .any(|pubkey| &StacksAddress::p2pkh(false, pubkey) == address) + }) + .collect(); + let (conf_1, _) = miners.get_node_configs(); let (miner_pk_1, miner_pk_2) = miners.get_miner_public_keys(); let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes(); - let rl1_skip_commit_op = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - let rl2_skip_commit_op = miners.rl2_counters.naka_skip_commit_op.clone(); - info!("------------------------- Pause Miner 2's Block Commits -------------------------"); - - // Make sure Miner 2 cannot win a sortition at first. - rl2_skip_commit_op.set(true); + miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); @@ -3552,8 +3501,8 @@ fn non_blocking_minority_configured_to_favour_incoming_miner() { let mut btc_blocks_mined = 0; info!("------------------------- Pause Miner 1's Block Commit -------------------------"); - // Make sure miner 1 doesn't submit any further block commits for the next tenure BEFORE mining the bitcoin block - rl1_skip_commit_op.set(true); + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); info!("------------------------- Miner 1 Mines a Normal Tenure A -------------------------"); miners @@ -3561,117 +3510,248 @@ fn non_blocking_minority_configured_to_favour_incoming_miner() { .expect("Failed to start Tenure A"); btc_blocks_mined += 1; - // assure we have a successful sortition that miner 1 won verify_sortition_winner(&sortdb, &miner_pkh_1); info!("------------------------- Submit Miner 2 Block Commit -------------------------"); let stacks_height_before = miners.get_peer_stacks_tip_height(); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); let burn_height_before = get_burn_height(); - // Pause the block proposal broadcast so that miner 2 AND miner 1 are unable to propose - // a block BEFORE block_proposal_timeout - TEST_BROADCAST_PROPOSAL_STALL.set(vec![miner_pk_2.clone(), miner_pk_1.clone()]); + + if minority_favours_incoming { + // Stall both miners so neither can propose before block_proposal_timeout + TEST_BROADCAST_PROPOSAL_STALL.set(vec![miner_pk_2.clone(), miner_pk_1.clone()]); + } else { + // Only stall miner 2 so miner 1 can attempt to extend + TEST_BROADCAST_PROPOSAL_STALL.set(vec![miner_pk_2.clone()]); + } info!("------------------------- Miner 2 Wins Tenure B -------------------------"; "burn_height_before" => burn_height_before, "stacks_height_before" => %stacks_height_before ); + test_observer::clear(); miners .mine_bitcoin_blocks_and_confirm(&sortdb, 1, 30) .expect("Failed to start Tenure B"); btc_blocks_mined += 1; - // assure we have a successful sortition that miner 2 won + assert_eq!(stacks_height_before, miners.get_peer_stacks_tip_height()); verify_sortition_winner(&sortdb, &miner_pkh_2); + wait_for_state_machine_update( + 30, + &get_chain_info(&conf_1).pox_consensus, + burn_height_before + 1, + Some((miner_pkh_2.clone(), stacks_height_before)), + &miners.signer_test.signer_addresses_versions(), + ) + .expect("Signers failed to update state machine to Miner 2's tenure win"); info!( - "------------------------- Wait for Miner 2 to be Marked Invalid by a Majority of Signers -------------------------" + "------------------------- Wait for Signers to Mark Incoming Miner as Invalid -------------------------" ); - // Make sure that miner 1 and a majority of signers thinks miner 2 is invalid. + + // Sleep to let the short-timeout signers mark the incoming miner as invalid std::thread::sleep(tenure_extend_wait_timeout.add(Duration::from_secs(1))); - // Allow miner 2 to attempt to start their tenure. - TEST_BROADCAST_PROPOSAL_STALL.set(vec![miner_pk_1.clone()]); + // Verify the short-timeout signers have updated their state machine. + // In the latest protocol, the minority reports miner 1 as the active miner (extending). + // In v1 / FavourIncoming, signers still report miner 2 as the winning miner. + let (expected_pkh, stacks_tip_height) = + if matches!(variant, NonBlockingMinorityVariant::FavourPrevMiner) { + (miner_pkh_1.clone(), stacks_height_before - 1) + } else { + (miner_pkh_2.clone(), stacks_height_before) + }; + wait_for_state_machine_update( + 30, + &get_chain_info(&conf_1).pox_consensus, + burn_height_before + 1, + Some((expected_pkh, stacks_tip_height)), + &short_timeout_signers, + ) + .expect("Short-timeout signers failed to update state machine"); - info!("------------------------- Wait for Miner 2's Block N+1' to be Proposed ------------------------"; - "stacks_height_before" => %stacks_height_before); + if minority_favours_incoming { + // FavourIncomingMiner: miner 2 proposes first and gets rejected, + // then miner 1 extends successfully - let miner_2_block_n_1 = - wait_for_block_proposal_block(30, stacks_height_before + 1, &miner_pk_2) - .expect("Miner 2 did not propose Block N+1'"); + // Clear stale observer data before unstalling so we only see new events + test_observer::clear(); + // Allow miner 2 to attempt to start their tenure (keep miner 1 stalled). + TEST_BROADCAST_PROPOSAL_STALL.set(vec![miner_pk_1.clone()]); - assert!(miner_2_block_n_1 - .try_get_tenure_change_payload() - .unwrap() - .cause - .is_eq(&TenureChangeCause::BlockFound)); + info!("------------------------- Wait for Miner 2's Block N+1' to be Proposed ------------------------"; + "stacks_height_before" => %stacks_height_before); - info!("------------------------- Verify that Miner 2's Block N+1' was Rejected ------------------------"); + let miner_2_block_n_1 = + wait_for_block_proposal_block(30, stacks_height_before + 1, &miner_pk_2) + .expect("Miner 2 did not propose Block N+1'"); - // Miner 2's proposed block should get rejected by the signers - wait_for_block_global_rejection( - 30, - &miner_2_block_n_1.header.signer_signature_hash(), - num_signers, - ) - .expect("Timed out waiting for Block N+1' to be globally rejected"); + assert!(miner_2_block_n_1 + .try_get_tenure_change_payload() + .unwrap() + .cause + .is_eq(&TenureChangeCause::BlockFound)); - assert_eq!(miners.get_peer_stacks_tip_height(), stacks_height_before,); + info!("------------------------- Verify that Miner 2's Block N+1' was Rejected ------------------------"); + wait_for_block_global_rejection( + 30, + &miner_2_block_n_1.header.signer_signature_hash(), + num_signers, + ) + .expect("Timed out waiting for Block N+1' to be globally rejected"); - info!("------------------------- Wait for Miner 1's Block N+1 Extended to be Mined ------------------------"; - "stacks_height_before" => %stacks_height_before - ); + assert_eq!(miners.get_peer_stacks_tip_height(), stacks_height_before); - TEST_BROADCAST_PROPOSAL_STALL.set(vec![]); + info!("------------------------- Wait for Miner 1's Block N+1 Extended to be Mined ------------------------"; + "stacks_height_before" => %stacks_height_before + ); - // Get miner 1's N+1 block proposal - let miner_1_block_n_1 = - wait_for_block_pushed_by_miner_key(30, stacks_height_before + 1, &miner_pk_1) + // Clear stale observer data before unstalling so we only see new events + test_observer::clear(); + TEST_BROADCAST_PROPOSAL_STALL.set(vec![]); + + let _miner_1_block_n_1 = + wait_for_block_pushed_and_tip(30, stacks_height_before + 1, &miner_pk_1, || { + miners.get_peer_info().stacks_tip + }) .expect("Timed out waiting for Miner 1 to mine N+1"); - let peer_info = miners.get_peer_info(); - assert_eq!(peer_info.stacks_tip, miner_1_block_n_1.header.block_hash()); - assert_eq!(peer_info.stacks_tip_height, stacks_height_before + 1); + info!( + "------------------------- Verify Extended in Miner 1's Block N+1 -------------------------" + ); + verify_last_block_contains_tenure_change_tx(TenureChangeCause::Extended); + } else { + // FavourPrevMiner / FavourPrevMinerV1: miner 1 extends first and gets rejected, + // then miner 2 proposes BlockFound and succeeds + + info!("------------------------- Wait for Miner 1's Block N+1' to be Proposed ------------------------"; + "stacks_height_before" => %stacks_height_before); + + let miner_1_block_n_1_prime = + wait_for_block_proposal_block(30, stacks_height_before + 1, &miner_pk_1) + .expect("Miner 1 failed to propose block N+1'"); + assert!(miner_1_block_n_1_prime + .try_get_tenure_change_payload() + .unwrap() + .cause + .is_eq(&TenureChangeCause::Extended)); + + info!("------------------------- Verify that Miner 1's Block N+1' was Rejected ------------------------"); + if matches!(variant, NonBlockingMinorityVariant::FavourPrevMiner) { + wait_for_block_rejections_from_signers( + 30, + &miner_1_block_n_1_prime.header.signer_signature_hash(), + &all_signers, + ) + .expect("Failed to reach rejection consensus for Miner 1's Block N+1'"); + } else { + wait_for_block_global_rejection( + 30, + &miner_1_block_n_1_prime.header.signer_signature_hash(), + num_signers, + ) + .expect("Failed to reach rejection consensus for Miner 1's Block N+1'"); + } - info!( - "------------------------- Verify BlockFound in Miner 1's Block N+1 -------------------------" - ); - verify_last_block_contains_tenure_change_tx(TenureChangeCause::Extended); + assert_eq!(stacks_height_before, miners.get_peer_stacks_tip_height()); - info!("------------------------- Miner 1 Mines Block N+2 with Transfer Tx -------------------------"); - let stacks_height_before = peer_info.stacks_tip_height; - // submit a tx so that the miner will mine an extra block - let _ = miners - .send_and_mine_transfer_tx(30) - .expect("Failed to mine transfer tx"); + info!("------------------------- Wait for Miner 2's Block N+1 BlockFound to be Proposed and Approved ------------------------"; + "stacks_height_before" => %stacks_height_before + ); - // Get miner 1's N+2 block proposal - let miner_1_block_n_2 = - wait_for_block_pushed_by_miner_key(30, stacks_height_before + 1, &miner_pk_1) - .expect("Timed out waiting for miner 1 to mine N+2"); + // Clear stale observer data before unstalling so we only see new events + test_observer::clear(); + TEST_BROADCAST_PROPOSAL_STALL.set(vec![]); - let peer_info = miners.get_peer_info(); - assert_eq!(peer_info.stacks_tip, miner_1_block_n_2.header.block_hash()); - assert_eq!(peer_info.stacks_tip_height, stacks_height_before + 1); + let miner_2_block_n_1 = + wait_for_block_pushed_and_tip(30, stacks_height_before + 1, &miner_pk_2, || { + miners.get_peer_info().stacks_tip + }) + .expect("Miner 2's block N+1 was not mined"); - info!("------------------------- Unpause Miner 2's Block Commits -------------------------"); - miners.submit_commit_miner_2(&sortdb); + if matches!(variant, NonBlockingMinorityVariant::FavourPrevMiner) { + info!( + "------------------------- Verify ALL Signers Accepted Miner 2's Block N+1 -------------------------" + ); + wait_for_block_acceptance_from_signers( + 30, + &miner_2_block_n_1.header.signer_signature_hash(), + &all_signers, + ) + .expect("Failed to get expected acceptances for Miner 2's block N+1."); + } else { + info!( + "------------------------- Verify Minority Rejected Miner 2's Block N+1 -------------------------" + ); + wait_for_block_rejections( + 30, + &miner_2_block_n_1.header.signer_signature_hash(), + non_block_minority, + ) + .expect("Failed to get expected rejections for Miner 2's block N+1."); + } - let burn_height_before = get_burn_height(); + info!( + "------------------------- Verify BlockFound in Miner 2's Block N+1 -------------------------" + ); + verify_last_block_contains_tenure_change_tx(TenureChangeCause::BlockFound); + } + + info!("------------------------- Mine Block N+2 with Transfer Tx -------------------------"); + let stacks_height_before = miners.get_peer_stacks_tip_height(); + miners + .send_and_mine_transfer_tx(30) + .expect("Failed to mine transfer tx"); + + // The continuing miner (miner 1 for FavourIncoming, miner 2 for FavourPrev) mines N+2 + let continuing_miner_pk = if minority_favours_incoming { + &miner_pk_1 + } else { + &miner_pk_2 + }; + let block_n_2 = + wait_for_block_pushed_and_tip(30, stacks_height_before + 1, continuing_miner_pk, || { + miners.get_peer_info().stacks_tip + }) + .expect("Timed out waiting for block N+2"); + + // V1 variant additionally verifies minority rejection for N+2 + if matches!(variant, NonBlockingMinorityVariant::FavourPrevMinerV1) { + info!( + "------------------------- Verify Minority Rejected Block N+2 -------------------------" + ); + wait_for_block_rejections( + 30, + &block_n_2.header.signer_signature_hash(), + non_block_minority, + ) + .expect("Failed to get expected rejections for block N+2."); + } - info!("------------------------- Miner 2 Mines a Normal Tenure C -------------------------"; - "burn_height_before" => burn_height_before); + info!("------------------------- Mine Tenure C -------------------------"); + if minority_favours_incoming { + // Miner 2 mines Tenure C + miners.ensure_commit_miner_2(&sortdb); + } else { + // Miner 1 mines Tenure C + miners.ensure_commit_miner_1(&sortdb); + } miners .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30) - .expect("Failed to mine BTC block followed by a tenure change tx"); + .expect("Failed to mine Tenure C"); btc_blocks_mined += 1; - // assure we have a successful sortition that miner 2 won - verify_sortition_winner(&sortdb, &miner_pkh_2); + let tenure_c_winner = if minority_favours_incoming { + &miner_pkh_2 + } else { + &miner_pkh_1 + }; + verify_sortition_winner(&sortdb, tenure_c_winner); info!( - "------------------------- Verify Tenure Change Tx in Miner 2's Block N+3 -------------------------" + "------------------------- Verify Tenure Change Tx in Block N+3 -------------------------" ); verify_last_block_contains_tenure_change_tx(TenureChangeCause::BlockFound); @@ -3686,236 +3766,75 @@ fn non_blocking_minority_configured_to_favour_incoming_miner() { miners.shutdown(); } -/// Test a scenario where a non-blocking majority of signers are configured to favour the previous miner -/// extending their tenure when the incoming miner is slow to propose a block. The incoming miner should succeed -/// and its subsequent blocks should be be approved. +/// Test a scenario where a non-blocking minority of signers are configured to favour the incoming miner. +/// The previous miner should extend its tenure and succeed as a majority are configured to favour it +/// and its subsequent blocks should be approved. /// Two miners boot to Nakamoto. /// Miner 1 wins the first tenure A. /// Miner 1 proposes a block N with a TenureChangeCause::BlockFound /// Signers accept and the stacks tip advances to N /// Miner 2 wins the second tenure B. -/// A minority of signers mark miner 2 as invalid. -/// Miner 1 proposes block N+1' with a TenureChangeCause::Extended -/// A majority of signers reject block N+1' -/// Miner 2 proposes block N+1 with a TenureChangeCause::BlockFound -/// A majority fo signers accept block N+1. -/// Miner 2 proposes block N+2 with a transfer tx -/// A majority of signers should accept block N+2. -/// Miner 1 wins the third tenure C. -/// Miner 1 proposes block N+3 with a TenureChangeCause::BlockFound +/// A majority of signers mark miner 2 as invalid. +/// Miner 2 proposes block N+1' with a TenureChangeCause::BlockFound +/// A majority of signers reject block N+1'. +/// Miner 1 proposes block N+1 with a TenureChangeCause::Extended +/// A majority of signers accept and the stacks tip advances to N+1 +/// Miner 1 proposes block N+2 with a transfer tx +/// ALL signers should accept block N+2. +/// Miner 2 wins the third tenure C. +/// Miner 2 proposes block N+3 with a TenureChangeCause::BlockFound /// Signers accept and the stacks tip advances to N+3 /// /// Asserts: /// - Block N contains the TenureChangeCause::BlockFound -/// - Block N+1' contains a TenureChangeCause::Extended and is rejected -/// - Block N+1 contains the TenureChangeCause::BlockFound +/// - Block N+1' contains a TenureChangeCause::BlockFound and is rejected +/// - Block N+1 contains the TenureChangeCause::Extended /// - Block N+2 is accepted. /// - Block N+3 contains the TenureChangeCause::BlockFound. /// - The stacks tip advances to N+3 #[test] #[ignore] -fn non_blocking_minority_configured_to_favour_prev_miner_v1() { - if env::var("BITCOIND_TEST") != Ok("1".into()) { - return; - } - - let num_signers = 5; - let non_block_minority = num_signers * 2 / 10; - let num_txs = 1; - - let favour_prev_miner_block_proposal_timeout = Duration::from_secs(20); - let favour_incoming_miner_block_proposal_timeout = Duration::from_secs(500); - // Make sure the miner attempts to extend after the minority mark the incoming as invalid - let tenure_extend_wait_timeout = favour_prev_miner_block_proposal_timeout; - - let mut miners = MultipleMinerTest::new_with_config_modifications( - num_signers, - num_txs, - |signer_config| { - let port = signer_config.endpoint.port(); - // Note signer ports are based on the number of them, the first being 3000, the last being 3000 + num_signers - 1 - if port < 3000 + non_block_minority as u16 { - signer_config.block_proposal_timeout = favour_prev_miner_block_proposal_timeout; - } else { - signer_config.block_proposal_timeout = favour_incoming_miner_block_proposal_timeout; - } - }, - |config| { - config.miner.tenure_extend_wait_timeout = tenure_extend_wait_timeout; - config.miner.block_commit_delay = Duration::from_secs(0); - }, - |config| { - config.miner.block_commit_delay = Duration::from_secs(0); - }, - ); - let all_signers = miners.signer_test.signer_test_pks(); - // Pin all the signers to version 1; - let pinned_signers = all_signers.iter().map(|key| (key.clone(), 1)).collect(); - TEST_PIN_SUPPORTED_SIGNER_PROTOCOL_VERSION.set(pinned_signers); - let (conf_1, _) = miners.get_node_configs(); - let (miner_pk_1, miner_pk_2) = miners.get_miner_public_keys(); - let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes(); - - let rl1_skip_commit_op = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - let rl2_skip_commit_op = miners.rl2_counters.naka_skip_commit_op.clone(); - - info!("------------------------- Pause Miner 2's Block Commits -------------------------"); - - // Make sure Miner 2 cannot win a sortition at first. - rl2_skip_commit_op.set(true); - - miners.boot_to_epoch_3(); - - let burnchain = conf_1.get_burnchain(); - let sortdb = burnchain.open_sortition_db(true).unwrap(); - - let get_burn_height = || { - SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()) - .unwrap() - .block_height - }; - let starting_peer_height = get_chain_info(&conf_1).stacks_tip_height; - let starting_burn_height = get_burn_height(); - let mut btc_blocks_mined = 0; - - info!("------------------------- Pause Miner 1's Block Commit -------------------------"); - // Make sure miner 1 doesn't submit any further block commits for the next tenure BEFORE mining the bitcoin block - rl1_skip_commit_op.set(true); - - info!("------------------------- Miner 1 Mines a Normal Tenure A -------------------------"); - miners - .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30) - .expect("Failed to mine BTC block and Tenure Change Tx Block"); - btc_blocks_mined += 1; - - // assure we have a successful sortition that miner 1 won - verify_sortition_winner(&sortdb, &miner_pkh_1); - - info!("------------------------- Submit Miner 2 Block Commit -------------------------"); - miners.submit_commit_miner_2(&sortdb); - // Pause the block proposal broadcast so that miner 2 will be unable to broadcast its - // tenure change proposal BEFORE miner 1 attempts to extend. - TEST_BROADCAST_PROPOSAL_STALL.set(vec![miner_pk_2.clone()]); - - let stacks_height_before = miners.get_peer_stacks_tip_height(); - info!("------------------------- Miner 2 Wins Tenure B -------------------------"; - "stacks_height_before" => %stacks_height_before); - miners - .mine_bitcoin_blocks_and_confirm(&sortdb, 1, 30) - .expect("Failed to start Tenure B"); - btc_blocks_mined += 1; - - assert_eq!(stacks_height_before, miners.get_peer_stacks_tip_height()); - - // assure we have a successful sortition that miner 2 won - verify_sortition_winner(&sortdb, &miner_pkh_2); - info!( - "------------------------- Wait for Miner 1 to think Miner 2 is Invalid -------------------------" - ); - // Make sure that miner 1 thinks miner 2 is invalid. - std::thread::sleep(tenure_extend_wait_timeout.add(Duration::from_secs(1))); - - info!("------------------------- Wait for Miner 1's Block N+1' to be Proposed ------------------------"; - "stacks_height_before" => %stacks_height_before); - - let miner_1_block_n_1_prime = - wait_for_block_proposal_block(30, stacks_height_before + 1, &miner_pk_1) - .expect("Miner 1 failed to propose block N+1'"); - assert!(miner_1_block_n_1_prime - .try_get_tenure_change_payload() - .unwrap() - .cause - .is_eq(&TenureChangeCause::Extended)); - - info!("------------------------- Verify that Miner 1's Block N+1' was Rejected ------------------------"); - wait_for_block_global_rejection( - 30, - &miner_1_block_n_1_prime.header.signer_signature_hash(), - num_signers, - ) - .expect("Failed to reach rejection consensus for Miner 1's Block N+1'"); - - assert_eq!(stacks_height_before, miners.get_peer_stacks_tip_height()); - - info!("------------------------- Wait for Miner 2's Block N+1 BlockFound to be Proposed and Approved------------------------"; - "stacks_height_before" => %stacks_height_before - ); - - TEST_BROADCAST_PROPOSAL_STALL.set(vec![]); - - let miner_2_block_n_1 = - wait_for_block_pushed_by_miner_key(30, stacks_height_before + 1, &miner_pk_2) - .expect("Miner 2's block N+1 was not mined"); - let peer_info = miners.get_peer_info(); - assert_eq!(peer_info.stacks_tip, miner_2_block_n_1.header.block_hash()); - assert_eq!(peer_info.stacks_tip_height, stacks_height_before + 1); - - info!("------------------------- Verify Minority of Signer's Rejected Miner 2's Block N+1 -------------------------"); - wait_for_block_rejections( - 30, - &miner_2_block_n_1.header.signer_signature_hash(), - non_block_minority, - ) - .expect("Failed to get expected rejections for Miner 2's block N+1."); - info!( - "------------------------- Verify BlockFound in Miner 2's Block N+1 -------------------------" - ); - verify_last_block_contains_tenure_change_tx(TenureChangeCause::BlockFound); - - info!("------------------------- Miner 2 Mines Block N+2 with Transfer Tx -------------------------"); - let stacks_height_before = miners.get_peer_stacks_tip_height(); - miners - .send_and_mine_transfer_tx(30) - .expect("Failed to Mine Block N+2"); - - let miner_2_block_n_2 = - wait_for_block_pushed_by_miner_key(30, stacks_height_before + 1, &miner_pk_2) - .expect("Miner 2's block N+1 was not mined"); - let peer_info = miners.get_peer_info(); - assert_eq!(peer_info.stacks_tip, miner_2_block_n_2.header.block_hash()); - assert_eq!(peer_info.stacks_tip_height, stacks_height_before + 1); - - info!( - "------------------------- Verify Miner 2's Block N+2 is still Rejected by Minority Signers -------------------------" +fn non_blocking_minority_configured_to_favour_incoming_miner() { + non_blocking_minority_configured_to_favour_test( + NonBlockingMinorityVariant::FavourIncomingMiner, ); - wait_for_block_rejections( - 30, - &miner_2_block_n_2.header.signer_signature_hash(), - non_block_minority, - ) - .expect("Failed to get expected rejections for Miner 2's block N+2."); - - info!("------------------------- Unpause Miner 1's Block Commits -------------------------"); - miners.submit_commit_miner_1(&sortdb); - - info!("------------------------- Miner 1 Mines a Normal Tenure C -------------------------"); - miners - .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30) - .expect("Failed to start Tenure C and mine block N+3"); - btc_blocks_mined += 1; - - // assure we have a successful sortition that miner 1 won - verify_sortition_winner(&sortdb, &miner_pkh_1); +} - info!( - "------------------------- Confirm Burn and Stacks Block Heights -------------------------" - ); - assert_eq!(get_burn_height(), starting_burn_height + btc_blocks_mined); - assert_eq!( - miners.get_peer_stacks_tip_height(), - starting_peer_height + 4 - ); - miners.shutdown(); +/// Test a scenario where a non-blocking minority of signers are configured to favour the previous miner +/// extending their tenure when the incoming miner is slow to propose a block. The incoming miner should succeed +/// and its subsequent blocks should be approved. +/// Two miners boot to Nakamoto. Signers pinned to protocol v1. +/// Miner 1 wins the first tenure A. +/// Miner 1 proposes a block N with a TenureChangeCause::BlockFound +/// Signers accept and the stacks tip advances to N +/// Miner 2 wins the second tenure B. +/// A minority of signers mark miner 2 as invalid. +/// Miner 1 proposes block N+1' with a TenureChangeCause::Extended +/// A majority of signers reject block N+1' +/// Miner 2 proposes block N+1 with a TenureChangeCause::BlockFound +/// A majority of signers accept block N+1. +/// Miner 2 proposes block N+2 with a transfer tx +/// A majority of signers should accept block N+2. +/// Miner 1 wins the third tenure C. +/// Miner 1 proposes block N+3 with a TenureChangeCause::BlockFound +/// Signers accept and the stacks tip advances to N+3 +/// +/// Asserts: +/// - Block N contains the TenureChangeCause::BlockFound +/// - Block N+1' contains a TenureChangeCause::Extended and is rejected +/// - Block N+1 contains the TenureChangeCause::BlockFound +/// - Block N+2 is accepted. +/// - Block N+3 contains the TenureChangeCause::BlockFound. +/// - The stacks tip advances to N+3 +#[test] +#[ignore] +fn non_blocking_minority_configured_to_favour_prev_miner_v1() { + non_blocking_minority_configured_to_favour_test(NonBlockingMinorityVariant::FavourPrevMinerV1); } -/// Test a scenario where a non-blocking majority of signers are configured to favour the previous miner +/// Test a scenario where a non-blocking minority of signers are configured to favour the previous miner /// extending their tenure when the incoming miner is slow to propose a block. The incoming miner should succeed -/// and its subsequent blocks should be be approved. +/// and its subsequent blocks should be approved. /// Two miners boot to Nakamoto. /// Miner 1 wins the first tenure A. /// Miner 1 proposes a block N with a TenureChangeCause::BlockFound @@ -3942,216 +3861,7 @@ fn non_blocking_minority_configured_to_favour_prev_miner_v1() { #[test] #[ignore] fn non_blocking_minority_configured_to_favour_prev_miner() { - if env::var("BITCOIND_TEST") != Ok("1".into()) { - return; - } - - let num_signers = 5; - let non_block_minority = num_signers * 2 / 10; - let num_txs = 1; - - let favour_prev_miner_block_proposal_timeout = Duration::from_secs(20); - let favour_incoming_miner_block_proposal_timeout = Duration::from_secs(500); - // Make sure the miner attempts to extend after the minority mark the incoming as invalid - let tenure_extend_wait_timeout = favour_prev_miner_block_proposal_timeout; - - let mut miners = MultipleMinerTest::new_with_config_modifications( - num_signers, - num_txs, - |signer_config| { - let port = signer_config.endpoint.port(); - // Note signer ports are based on the number of them, the first being 3000, the last being 3000 + num_signers - 1 - if port < 3000 + non_block_minority as u16 { - signer_config.block_proposal_timeout = favour_prev_miner_block_proposal_timeout; - } else { - signer_config.block_proposal_timeout = favour_incoming_miner_block_proposal_timeout; - } - }, - |config| { - config.miner.tenure_extend_wait_timeout = tenure_extend_wait_timeout; - config.miner.block_commit_delay = Duration::from_secs(0); - }, - |config| { - config.miner.block_commit_delay = Duration::from_secs(0); - }, - ); - let all_signers = miners.signer_test.signer_test_pks(); - let non_blocking_minority_signers = &all_signers[..non_block_minority]; - let non_blocking_signer_versions: Vec<_> = miners - .signer_test - .signer_addresses_versions() - .into_iter() - .filter(|(address, _)| { - non_blocking_minority_signers - .iter() - .find(|pubkey| &StacksAddress::p2pkh(false, pubkey) == address) - .is_some() - }) - .collect(); - let (conf_1, _) = miners.get_node_configs(); - let (miner_pk_1, miner_pk_2) = miners.get_miner_public_keys(); - let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes(); - - let rl1_skip_commit_op = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - let rl2_skip_commit_op = miners.rl2_counters.naka_skip_commit_op.clone(); - - info!("------------------------- Pause Miner 2's Block Commits -------------------------"); - - // Make sure Miner 2 cannot win a sortition at first. - rl2_skip_commit_op.set(true); - - miners.boot_to_epoch_3(); - - let burnchain = conf_1.get_burnchain(); - let sortdb = burnchain.open_sortition_db(true).unwrap(); - - let get_burn_height = || { - SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()) - .unwrap() - .block_height - }; - let starting_peer_height = get_chain_info(&conf_1).stacks_tip_height; - let starting_burn_height = get_burn_height(); - let mut btc_blocks_mined = 0; - - info!("------------------------- Pause Miner 1's Block Commit -------------------------"); - // Make sure miner 1 doesn't submit any further block commits for the next tenure BEFORE mining the bitcoin block - rl1_skip_commit_op.set(true); - - info!("------------------------- Miner 1 Mines a Normal Tenure A -------------------------"); - miners - .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30) - .expect("Failed to mine BTC block and Tenure Change Tx Block"); - btc_blocks_mined += 1; - - // assure we have a successful sortition that miner 1 won - verify_sortition_winner(&sortdb, &miner_pkh_1); - - info!("------------------------- Submit Miner 2 Block Commit -------------------------"); - miners.submit_commit_miner_2(&sortdb); - // Pause the block proposal broadcast so that miner 2 will be unable to broadcast its - // tenure change proposal BEFORE miner 1 attempts to extend. - TEST_BROADCAST_PROPOSAL_STALL.set(vec![miner_pk_2.clone()]); - - let stacks_height_before = miners.get_peer_stacks_tip_height(); - info!("------------------------- Miner 2 Wins Tenure B -------------------------"; - "stacks_height_before" => %stacks_height_before); - test_observer::clear(); - miners - .mine_bitcoin_blocks_and_confirm(&sortdb, 1, 30) - .expect("Failed to start Tenure B"); - btc_blocks_mined += 1; - - assert_eq!(stacks_height_before, miners.get_peer_stacks_tip_height()); - - // assure we have a successful sortition that miner 2 won - verify_sortition_winner(&sortdb, &miner_pkh_2); - info!( - "------------------------- Wait for Miner 1 to think Miner 2 is Invalid -------------------------" - ); - // Make sure that miner 1 thinks miner 2 is invalid. - std::thread::sleep(tenure_extend_wait_timeout.add(Duration::from_secs(1))); - let get_burn_consensus_hash = || { - SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()) - .unwrap() - .consensus_hash - }; - // Lets make sure our non blocking minority tries to mark the miner invalid - wait_for_state_machine_update( - 30, - &get_burn_consensus_hash(), - miners.get_peer_info().burn_block_height, - Some((miner_pkh_1.clone(), stacks_height_before - 1)), - &non_blocking_signer_versions, - ) - .expect("Timed out waiting for minority signers to send a state update"); - - info!("------------------------- Wait for Miner 1's Block N+1' to be Proposed ------------------------"; - "stacks_height_before" => %stacks_height_before); - - let miner_1_block_n_1_prime = - wait_for_block_proposal_block(30, stacks_height_before + 1, &miner_pk_1) - .expect("Miner 1 failed to propose block N+1'"); - assert!(miner_1_block_n_1_prime - .try_get_tenure_change_payload() - .unwrap() - .cause - .is_eq(&TenureChangeCause::Extended)); - - info!("------------------------- Verify that Miner 1's Block N+1' was Rejected by ALL signers ------------------------"); - wait_for_block_rejections_from_signers( - 30, - &miner_1_block_n_1_prime.header.signer_signature_hash(), - &all_signers, - ) - .expect("Failed to reach rejection consensus for Miner 1's Block N+1'"); - - assert_eq!(stacks_height_before, miners.get_peer_stacks_tip_height()); - - info!("------------------------- Wait for Miner 2's Block N+1 BlockFound to be Proposed and Approved------------------------"; - "stacks_height_before" => %stacks_height_before - ); - - TEST_BROADCAST_PROPOSAL_STALL.set(vec![]); - - let miner_2_block_n_1 = - wait_for_block_pushed_by_miner_key(30, stacks_height_before + 1, &miner_pk_2) - .expect("Miner 2's block N+1 was not mined"); - let peer_info = miners.get_peer_info(); - assert_eq!(peer_info.stacks_tip, miner_2_block_n_1.header.block_hash()); - assert_eq!(peer_info.stacks_tip_height, stacks_height_before + 1); - - info!("------------------------- Verify ALL the Signer's Accepted Miner 2's Block N+1 -------------------------"); - wait_for_block_acceptance_from_signers( - 30, - &miner_2_block_n_1.header.signer_signature_hash(), - &all_signers, - ) - .expect("Failed to get expected acceptances for Miner 2's block N+1."); - info!( - "------------------------- Verify BlockFound in Miner 2's Block N+1 -------------------------" - ); - verify_last_block_contains_tenure_change_tx(TenureChangeCause::BlockFound); - - info!("------------------------- Miner 2 Mines Block N+2 with Transfer Tx -------------------------"); - let stacks_height_before = miners.get_peer_stacks_tip_height(); - miners - .send_and_mine_transfer_tx(30) - .expect("Failed to Mine Block N+2"); - - let miner_2_block_n_2 = - wait_for_block_pushed_by_miner_key(30, stacks_height_before + 1, &miner_pk_2) - .expect("Miner 2's block N+1 was not mined"); - let peer_info = miners.get_peer_info(); - assert_eq!(peer_info.stacks_tip, miner_2_block_n_2.header.block_hash()); - assert_eq!(peer_info.stacks_tip_height, stacks_height_before + 1); - - info!("------------------------- Unpause Miner 1's Block Commits -------------------------"); - miners.submit_commit_miner_1(&sortdb); - - info!("------------------------- Miner 1 Mines a Normal Tenure C -------------------------"); - miners - .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30) - .expect("Failed to start Tenure C and mine block N+3"); - btc_blocks_mined += 1; - - // assure we have a successful sortition that miner 1 won - verify_sortition_winner(&sortdb, &miner_pkh_1); - - info!( - "------------------------- Confirm Burn and Stacks Block Heights -------------------------" - ); - assert_eq!(get_burn_height(), starting_burn_height + btc_blocks_mined); - assert_eq!( - miners.get_peer_stacks_tip_height(), - starting_peer_height + 4 - ); - miners.shutdown(); + non_blocking_minority_configured_to_favour_test(NonBlockingMinorityVariant::FavourPrevMiner); } #[test] @@ -4188,7 +3898,7 @@ fn empty_sortition_before_approval() { let Counters { naka_submitted_commits: commits_submitted, naka_proposed_blocks: proposed_blocks, - naka_skip_commit_op: skip_commit_op, + skip_commit_op, .. } = signer_test.running_nodes.counters.clone(); @@ -4313,11 +4023,7 @@ fn empty_sortition_before_proposal() { SignerTest::new(num_signers, vec![(sender_addr, send_amt + send_fee)]); let http_origin = format!("http://{}", &signer_test.running_nodes.conf.node.rpc_bind); - let skip_commit_op = signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); + let skip_commit_op = signer_test.running_nodes.counters.skip_commit_op.clone(); signer_test.boot_to_epoch_3(); @@ -4487,14 +4193,12 @@ fn continue_after_fast_block_no_sortition() { let Counters { naka_rejected_blocks: rl1_rejections, - naka_skip_commit_op: rl1_skip_commit_op, naka_submitted_commits: rl1_commits, naka_mined_blocks: blocks_mined1, .. } = miners.signer_test.running_nodes.counters.clone(); let Counters { - naka_skip_commit_op: rl2_skip_commit_op, naka_submitted_commits: rl2_commits, naka_mined_blocks: blocks_mined2, .. @@ -4503,7 +4207,7 @@ fn continue_after_fast_block_no_sortition() { info!("------------------------- Pause Miner 2's Block Commits -------------------------"); // Make sure Miner 2 cannot win a sortition at first. - rl2_skip_commit_op.set(true); + miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); @@ -4521,8 +4225,8 @@ fn continue_after_fast_block_no_sortition() { let mut btc_blocks_mined = 0; info!("------------------------- Pause Miner 1's Block Commit -------------------------"); - // Make sure miner 1 doesn't submit any further block commits for the next tenure BEFORE mining the bitcoin block - rl1_skip_commit_op.set(true); + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); info!("------------------------- Miner 1 Mines a Normal Tenure A -------------------------"); miners @@ -4543,7 +4247,7 @@ fn continue_after_fast_block_no_sortition() { info!("------------------------- Submit Miner 2 Block Commit -------------------------"); let rejections_before = rl1_rejections.load(Ordering::SeqCst); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); let burn_height_before = get_burn_height(); info!("------------------------- Miner 2 Mines an Empty Tenure B -------------------------"; @@ -4629,12 +4333,10 @@ fn continue_after_fast_block_no_sortition() { info!("------------------------- Wait for Miner B's Block N+2 -------------------------"); let miner_2_block_n_2 = - wait_for_block_pushed_by_miner_key(30, stacks_height_before + 2, &miner_pk_2) - .expect("Did not mine Miner 2's Block N+2"); - assert_eq!( - miners.get_peer_stacks_tip(), - miner_2_block_n_2.header.block_hash() - ); + wait_for_block_pushed_and_tip(30, stacks_height_before + 2, &miner_pk_2, || { + miners.get_peer_info().stacks_tip + }) + .expect("Did not mine Miner 2's Block N+2"); info!("------------------------- Verify Miner B's Block N+2 -------------------------"); assert!(miner_2_block_n_2 @@ -4652,7 +4354,10 @@ fn continue_after_fast_block_no_sortition() { info!("------------------------- Verify Miner B's Block N+3 -------------------------"); - let block_n_3 = wait_for_block_pushed_by_miner_key(30, stacks_height_before + 3, &miner_pk_2) + let block_n_3 = + wait_for_block_pushed_and_tip(30, stacks_height_before + 3, &miner_pk_2, || { + miners.get_peer_info().stacks_tip + }) .expect("Did not mine Miner 2's Block N+3"); assert!(block_n_3 .txs @@ -4668,7 +4373,7 @@ fn continue_after_fast_block_no_sortition() { btc_blocks_mined += 1; info!("------------------------- Unpause Miner A's Block Commits -------------------------"); - miners.submit_commit_miner_1(&sortdb); + miners.ensure_commit_miner_1(&sortdb); info!("------------------------- Run Miner A's Tenure -------------------------"); miners @@ -4682,6 +4387,11 @@ fn continue_after_fast_block_no_sortition() { info!( "------------------------- Confirm Burn and Stacks Block Heights -------------------------" ); + wait_for(30, || { + let info = miners.get_peer_info(); + Ok(info.stacks_tip_height >= starting_peer_height + 6) + }) + .expect("Tip did not advance to expected height"); let peer_info = miners.get_peer_info(); assert_eq!(get_burn_height(), starting_burn_height + btc_blocks_mined); @@ -4792,8 +4502,12 @@ fn multiple_miners_empty_sortition() { // to get timed out. signer_config.block_proposal_timeout = Duration::from_secs(600); }, - |_| {}, - |_| {}, + |config| { + config.miner.block_commit_delay = Duration::from_secs(0); + }, + |config| { + config.miner.block_commit_delay = Duration::from_secs(0); + }, ); let (conf_1, _conf_2) = miners.get_node_configs(); @@ -4847,7 +4561,7 @@ fn multiple_miners_empty_sortition() { Ok(get_chain_info(&conf_1).stacks_tip_height > tenure_0_stacks_height) }) .expect("Timed out waiting for Miner 1 to mine the first block of Tenure 1"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); for _ in 0..2 { miners @@ -5064,15 +4778,15 @@ fn read_count_extend_after_burn_view_change() { miners.pause_commits_miner_2(); miners.boot_to_epoch_3(); - miners.pause_commits_miner_1(); - let sortdb = conf_1.get_burnchain().open_sortition_db(true).unwrap(); + miners.ensure_commit_miner_1(&sortdb); + miners.pause_commits_miner_1(); miners .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 60) .unwrap(); - miners.submit_commit_miner_1(&sortdb); + miners.ensure_commit_miner_1(&sortdb); info!("------------------------- Miner 1 Wins Tenure A -------------------------"); miners @@ -5084,7 +4798,7 @@ fn read_count_extend_after_burn_view_change() { let prev_tip = get_chain_info(&conf_1); info!("------------------------- Miner 2 Wins Tenure B -------------------------"); - miners.submit_commit_miner_2(&sortdb); + miners.ensure_commit_miner_2(&sortdb); miners .mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 60) .unwrap(); @@ -5095,7 +4809,8 @@ fn read_count_extend_after_burn_view_change() { info!("------------------------- Miner 1 Wins Tenure C with stale commit -------------------------"); - // We can't use `submit_commit_miner_1` here because we are using the stale view + miners.unpause_commits_miner_1(); + // We can't use `ensure_commit_miner_1` here because we are using the stale view { TEST_MINER_COMMIT_TIP.set(Some((prev_tip.pox_consensus, prev_tip.stacks_tip))); let rl1_commits_before = miners @@ -5109,7 +4824,7 @@ fn read_count_extend_after_burn_view_change() { .signer_test .running_nodes .counters - .naka_skip_commit_op + .skip_commit_op .set(false); wait_for(30, || { @@ -5134,7 +4849,7 @@ fn read_count_extend_after_burn_view_change() { .signer_test .running_nodes .counters - .naka_skip_commit_op + .skip_commit_op .set(true); TEST_MINER_COMMIT_TIP.set(None); } diff --git a/stacks-node/src/tests/signer/v0/tx_replay.rs b/stacks-node/src/tests/signer/v0/tx_replay.rs index eb31f72d5f6..c45d09beda1 100644 --- a/stacks-node/src/tests/signer/v0/tx_replay.rs +++ b/stacks-node/src/tests/signer/v0/tx_replay.rs @@ -91,8 +91,6 @@ fn tx_replay_forking_test() { ); let conf = &signer_test.running_nodes.conf; let http_origin = format!("http://{}", &conf.node.rpc_bind); - let stacks_miner_pk = StacksPublicKey::from_private(&conf.miner.mining_key.clone().unwrap()); - let btc_controller = &signer_test.running_nodes.btc_regtest_controller; if signer_test.bootstrap_snapshot() { @@ -226,46 +224,47 @@ fn tx_replay_forking_test() { signer_test.wait_for_replay_set_eq(30, expected_tx_replay_txids.clone()); info!("---- Mining post-fork block to clear tx replay set ----"); - let tip_after_fork = get_chain_info(&conf); - let stacks_height_before = tip_after_fork.stacks_tip_height; test_observer::clear(); fault_injection_unstall_miner(); - let expected_height = stacks_height_before + 2; - info!( - "---- Waiting for block pushed at height: {:?} ----", - expected_height - ); - - let block = wait_for_block_pushed_by_miner_key(60, expected_height, &stacks_miner_pk) - .expect("Timed out waiting for block pushed after fork"); - - info!("---- Block: {:?} ----", block); + // Wait for the replay set to be fully cleared (all replayed txs mined) + signer_test + .wait_for_signer_state_check(60, |state| Ok(state.get_tx_replay_set().is_none())) + .expect("Timed out waiting for tx replay set to be cleared"); - for (block_tx, expected_txid) in block - .txs + // Verify that all expected replayed txs were mined in the correct + // relative order across the post-fork blocks, and that no other + // user transactions were mined before them. The txs may land in + // different blocks depending on timing, so collect user txids from + // all observed blocks in block-height order and check ordering. + let mined_user_txids: Vec = test_observer::get_blocks() .iter() - .filter(|tx| { - // In this case, the miner issued a tenure extend in the block, - // because it's continuing a late tenure. - !matches!( - tx.payload, - TransactionPayload::TenureChange(TenureChangePayload { - cause: TenureChangeCause::Extended, - .. + .map(|block| { + let block: StacksBlockEvent = + serde_json::from_value(block.clone()).expect("Failed to parse block"); + block + .transactions + .iter() + .filter(|tx| { + !matches!( + tx.payload, + TransactionPayload::Coinbase(..) | TransactionPayload::TenureChange(..) + ) }) - ) + .map(|tx| tx.txid().to_hex()) + .collect::>() }) - .zip(expected_tx_replay_txids.iter()) - { - assert_eq!(block_tx.txid().to_hex(), *expected_txid); - } + .flatten() + .collect(); - signer_test - .wait_for_signer_state_check(30, |state| Ok(state.get_tx_replay_set().is_none())) - .expect("Timed out waiting for tx replay set to be cleared"); + // Replay txs must be the first user transactions mined, in order + assert_eq!( + &mined_user_txids[..expected_tx_replay_txids.len()], + expected_tx_replay_txids.as_slice(), + "Replay txs should be the first user transactions mined, in the expected order" + ); signer_test.shutdown(); } diff --git a/stacks-signer/CHANGELOG.md b/stacks-signer/CHANGELOG.md index 21158f353cc..7c33948b8ad 100644 --- a/stacks-signer/CHANGELOG.md +++ b/stacks-signer/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to the versioning scheme outlined in the [README.md](README.md). +## [3.4.0.0.0.0] + +### Fixed +- Fixed signer database migration 19 that could leave the database in corrupted, unrecoverable state. + ## [3.3.0.0.6.0] ### Added diff --git a/stacks-signer/changelog.d/README.md b/stacks-signer/changelog.d/README.md new file mode 100644 index 00000000000..2370b990a9f --- /dev/null +++ b/stacks-signer/changelog.d/README.md @@ -0,0 +1,34 @@ +# Changelog Fragments (Signer) + +Instead of editing `CHANGELOG.md` directly, each PR should add a **fragment +file** to this directory. This avoids merge conflicts and makes the release +process clearer. + +## How to add a changelog entry + +1. Create a file in this directory named: `-.` + + **Categories:** `added`, `changed`, `fixed`, `removed` + + **Examples:** + - `6800-track-pending-blocks.added` + - `6801-db-schema-v19.changed` + +2. Write the changelog entry text in the file (one or more lines of markdown): + + ``` + Added support for tracking pending block responses in the signer database + ``` + +3. That's it. The fragment will be assembled into `stacks-signer/CHANGELOG.md` + at release time using `contrib/assemble-changelog.sh`. + +## Notes + +- One fragment per PR is typical, but you can add multiple if your PR spans + categories. +- If your PR doesn't need a changelog entry (e.g., docs-only, CI changes, + test-only), you can skip this. Add the `no changelog` label to your PR to + bypass the CI check. +- Fragment files are deleted after they are assembled into the changelog during + a release. diff --git a/stacks-signer/src/chainstate/v1.rs b/stacks-signer/src/chainstate/v1.rs index ac1f0ab66d5..75b85cf348a 100644 --- a/stacks-signer/src/chainstate/v1.rs +++ b/stacks-signer/src/chainstate/v1.rs @@ -459,6 +459,19 @@ impl SortitionsView { signer_db: &mut SignerDb, client: &StacksClient, ) -> Result<(), RejectReason> { + // Check that the tenure change's prev_tenure matches the sortition's known parent tenure. + let parent_tenure_id = &proposed_by.state().data.parent_tenure_id; + if &tenure_change.prev_tenure_consensus_hash != parent_tenure_id { + warn!( + "Block commit parent tenure mismatch: the block commit's parent_block_ptr does not correspond to the actual parent tenure"; + "committed_parent_tenure" => %parent_tenure_id, + "actual_parent_tenure" => %tenure_change.prev_tenure_consensus_hash, + "consensus_hash" => %block.header.consensus_hash, + "signer_signature_hash" => %block.header.signer_signature_hash(), + ); + return Err(RejectReason::InvalidParentBlock); + } + // Ensure that the tenure change block confirms the expected parent block let confirms_expected_parent = SortitionData::check_tenure_change_confirms_parent( tenure_change, diff --git a/stacks-signer/src/chainstate/v2.rs b/stacks-signer/src/chainstate/v2.rs index fca97463cb5..1420eb0e3f9 100644 --- a/stacks-signer/src/chainstate/v2.rs +++ b/stacks-signer/src/chainstate/v2.rs @@ -170,6 +170,7 @@ impl GlobalStateView { Self::validate_tenure_change_payload( tenure_change, block, + parent_tenure_id, signer_db, client, &self.config, @@ -298,10 +299,23 @@ impl GlobalStateView { fn validate_tenure_change_payload( tenure_change: &TenureChangePayload, block: &NakamotoBlock, + parent_tenure_id: &ConsensusHash, signer_db: &mut SignerDb, client: &StacksClient, config: &ProposalEvalConfig, ) -> Result<(), RejectReason> { + // Check that the tenure change's prev_tenure matches the signer's known parent tenure. + if &tenure_change.prev_tenure_consensus_hash != parent_tenure_id { + warn!( + "Block commit parent tenure mismatch: the block commit's parent_block_ptr does not correspond to the actual parent tenure"; + "committed_parent_tenure" => %parent_tenure_id, + "actual_parent_tenure" => %tenure_change.prev_tenure_consensus_hash, + "consensus_hash" => %block.header.consensus_hash, + "signer_signature_hash" => %block.header.signer_signature_hash(), + ); + return Err(RejectReason::InvalidParentBlock); + } + // Ensure that the tenure change block confirms the expected parent block let confirms_expected_parent = SortitionData::check_tenure_change_confirms_parent( tenure_change, diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index 65f661e7dae..9f7d6da022b 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -276,66 +276,199 @@ pub struct GlobalConfig { pub supported_signer_protocol_version: u64, } -/// Internal struct for loading up the config file +/// Internal struct for loading up the config file. +/// +/// This struct represents the TOML configuration file format for the +/// `stacks-signer` binary. All fields with `Option` types will use their +/// documented defaults when omitted. #[derive(Deserialize, Debug)] struct RawConfigFile { - /// endpoint to stacks node + /// The Stacks node RPC endpoint that this signer will connect to. + /// --- + /// @default: (required, no default) + /// @notes: + /// - Format: `"host:port"` (e.g., `"127.0.0.1:20443"`). + /// - Must point to the `rpc_bind` address of the Stacks node. pub node_host: String, - /// endpoint to event receiver + /// The local endpoint the signer will listen on for events from the Stacks node. + /// --- + /// @default: (required, no default) + /// @notes: + /// - Format: `"host:port"` (e.g., `"0.0.0.0:30000"`). + /// - Must match the `endpoint` in the node's `[[events_observer]]` section. pub endpoint: String, /// The hex representation of the signer's Stacks private key used for communicating /// with the Stacks Node, including writing to the Stacker DB instance. + /// --- + /// @default: (required, no default) + /// @notes: + /// - 64 or 66 hex characters (with optional `01` compression suffix). + /// - This key determines the signer's on-chain identity and address. pub stacks_private_key: String, - /// The network to use. One of "mainnet" or "testnet". + /// The network to use. One of `"mainnet"`, `"testnet"`, or `"mocknet"`. + /// --- + /// @default: (required, no default) + /// @notes: + /// - Determines address version and transaction version. pub network: Network, - /// The time to wait (in millisecs) for a response from the stacker-db instance + /// The time to wait for a response from the stacker-db instance. + /// --- + /// @default: `5_000` + /// @units: milliseconds pub event_timeout_ms: Option, - /// The authorization password for the block proposal endpoint + /// The authorization password for the block proposal endpoint. + /// --- + /// @default: (required, no default) + /// @notes: + /// - WARNING: Must match the `auth_token` in the Stacks node's + /// `[connection_options]` section. If these do not match, the signer + /// cannot communicate with the node. pub auth_password: String, - /// The path to the signer's database file or :memory: for an in-memory database + /// The path to the signer's database file or `:memory:` for an in-memory database. + /// --- + /// @default: (required, no default) + /// @notes: + /// - Use an absolute path for production (e.g., `"/var/lib/stacks-signer/signerdb.sqlite"`). + /// - Use `":memory:"` only for testing. pub db_path: String, - /// Metrics endpoint + /// Optional Prometheus metrics endpoint. + /// --- + /// @default: `None` (disabled) + /// @notes: + /// - Format: `"host:port"` (e.g., `"0.0.0.0:9090"`). pub metrics_endpoint: Option, - /// How much time (in secs) must pass between the first block proposal in a tenure and the next bitcoin block - /// before a subsequent miner isn't allowed to reorg the tenure + /// Reorg protection window. Measures the time between when a tenure's first block + /// was signed and when the next burn block arrived. + /// + /// If `(burn_block_received - first_block_signed) < this value`, the signer allows + /// a new miner to reorg the tenure. Otherwise, the tenure is considered established + /// and the reorg is denied. + /// --- + /// @default: `60` + /// @units: seconds + /// @notes: + /// - WARNING: Setting too low allows reorgs of established tenures. + /// Setting too high blocks legitimate miner handoffs. pub first_proposal_burn_block_timing_secs: Option, - /// How much time (in millisecs) to wait for a miner to propose a block following a sortition + /// How long to wait for the current sortition winner to propose a block before + /// the signer marks that miner as inactive (`InvalidatedBeforeFirstBlock`). + /// + /// This is one of two gates for accepting tenure extends from the previous miner. + /// The signer will not accept a tenure extend until both this timeout fires + /// (invalidating the unresponsive new winner) AND `tenure_idle_timeout + buffer` + /// has passed since the last block. + /// --- + /// @default: `120_000` + /// @units: milliseconds + /// @notes: + /// - WARNING: Interacts with miner's `tenure_extend_wait_timeout_ms` (default 120_000ms). + /// If the miner's value is lower, the miner extends before the signer invalidates + /// the new sortition winner, causing the extend to be rejected. pub block_proposal_timeout_ms: Option, - /// An optional custom Chain ID + /// An optional custom Chain ID. Overrides the default for the selected network. + /// --- + /// @default: `0x00000001` (mainnet) or `0x80000000` (testnet) + /// @notes: + /// - Only set this for custom/private networks. pub chain_id: Option, - /// Time in seconds to wait for the last block of a tenure to be globally accepted or rejected + /// Time to wait for the last block of a tenure to be globally accepted or rejected /// before considering a new miner's block at the same height as potentially valid. + /// --- + /// @default: `30` + /// @units: seconds pub tenure_last_block_proposal_timeout_secs: Option, - /// How long to wait (in millisecs) for a response from a block proposal validation response from the node - /// before marking that block as invalid and rejecting it + /// How long to wait for a response from a block proposal validation response from + /// the node before marking that block as invalid and rejecting it. + /// --- + /// @default: `120_000` + /// @units: milliseconds pub block_proposal_validation_timeout_ms: Option, - /// How much idle time (in seconds) must pass before a tenure extend is allowed + /// How much time since the last block in a tenure must pass before the signer + /// will allow a tenure extend. + /// + /// The signer computes: `extend_timestamp = last_block_time + this + buffer` + /// and includes it in the `BlockAccepted` response. The miner cannot extend + /// until `current_time >= extend_timestamp`. + /// + /// This is one of two gates for tenure extends (the other is + /// `block_proposal_timeout` for new-winner invalidation). + /// --- + /// @default: `60` + /// @units: seconds + /// @notes: + /// - WARNING: Must coordinate with miner's `tenure_timeout` (default 180s, + /// must be > this + buffer) and `tenure_extend_wait_timeout_ms` + /// (default 120_000ms, should be >= this + buffer). pub tenure_idle_timeout_secs: Option, - /// How much idle time (in seconds) must pass before a read-count tenure extend is allowed + /// How much idle time must pass before allowing a read-count tenure extend. + /// A read-count tenure extend is triggered when the read count budget is nearly + /// exhausted. + /// --- + /// @default: `20` + /// @units: seconds pub read_count_idle_timeout_secs: Option, - /// Number of seconds of buffer to add to the tenure extend time sent to miners to allow for - /// clock skew + /// Buffer time added to the tenure extend time sent to miners to account for + /// clock skew between signer and miner nodes. + /// --- + /// @default: `2` + /// @units: seconds + /// @notes: + /// - Increase if signer and miner clocks are poorly synchronized. pub tenure_idle_timeout_buffer_secs: Option, - /// The maximum age of a block proposal (in secs) that will be processed by the signer. + /// The maximum age of a block proposal that will be processed by the signer. + /// Proposals older than this are ignored. + /// --- + /// @default: `600` + /// @units: seconds pub block_proposal_max_age_secs: Option, - /// Time (in millisecs) following a block's global acceptance that a signer will consider an attempt by a miner - /// to reorg the block as valid towards miner activity + /// Time following a block's global acceptance during which a signer will consider + /// a miner's attempt to reorg it as valid miner activity. + /// --- + /// @default: `200_000` + /// @units: milliseconds pub reorg_attempts_activity_timeout_ms: Option, - /// Time to wait (in millisecs) before submitting a block proposal to the stacks-node + /// Time to wait before submitting a block proposal to the stacks-node if we cannot + /// determine that the stacks-node has processed the parent block. + /// --- + /// @default: `15` + /// @units: seconds pub proposal_wait_for_parent_time_secs: Option, - /// Is this signer binary going to be running in dry-run mode? + /// Run in dry-run mode. In dry-run mode, the signer will not submit + /// StackerDB messages or participate in signing, but will log what it + /// would have done. + /// --- + /// @default: `false` pub dry_run: Option, - /// Whether or not to validate blocks with replay transactions + /// Whether to validate blocks by replaying transactions. + /// --- + /// @default: `false` + /// @notes: + /// - Experimental feature. Provides additional validation but increases + /// resource usage. pub validate_with_replay_tx: Option, - /// How many blocks after a fork should we reset the replay set, - /// as a failsafe mechanism + /// Number of blocks after a fork to reset the replay set as a failsafe mechanism. + /// --- + /// @default: `2` + /// @units: blocks pub reset_replay_set_after_fork_blocks: Option, - /// Time to wait (in secs) between updating our local state machine view point and capitulating to other signers miner view + /// Time to wait between updating the local state machine view and capitulating + /// to other signers' tenure view. + /// --- + /// @default: `20` + /// @units: seconds + /// @notes: + /// - Controls how quickly a signer will adopt the consensus view when its + /// local view differs from the majority. pub capitulate_miner_view_timeout_secs: Option, - /// Time to wait (in secs) before timing out an HTTP request with StackerDB. + /// HTTP timeout for read/write operations with StackerDB. + /// --- + /// @default: `120` + /// @units: seconds pub stackerdb_timeout_secs: Option, #[cfg(any(test, feature = "testing"))] - /// Only used for testing to enable specific signer protocol versions + /// Only used for testing to enable specific signer protocol versions. + /// --- + /// @default: `SUPPORTED_SIGNER_PROTOCOL_VERSION` pub supported_signer_protocol_version: Option, } @@ -691,6 +824,28 @@ chain_id = {chain_id} mod tests { use super::*; + #[test] + fn test_example_confs() { + // Validate that all sample signer config files in sample/conf/signer/ parse as valid TOML. + // Uses RawConfigFile (not GlobalConfig) since reference configs have placeholder values. + let conf_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../sample/conf/signer"); + println!("Reading signer config files from: {conf_dir:?}"); + let conf_files = fs::read_dir(&conf_dir).unwrap(); + + for entry in conf_files { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_file() { + let file_name = path.file_name().unwrap().to_str().unwrap(); + if file_name.ends_with(".toml") { + let data = fs::read_to_string(&path).unwrap(); + RawConfigFile::load_from_str(&data) + .unwrap_or_else(|e| panic!("Failed to parse {file_name}: {e}")); + } + } + } + } + #[test] fn build_signer_config_tomls_should_produce_deserializable_strings() { let pk = StacksPrivateKey::from_hex( diff --git a/stacks-signer/src/signerdb.rs b/stacks-signer/src/signerdb.rs index 10f654e30e2..d811510f9a4 100644 --- a/stacks-signer/src/signerdb.rs +++ b/stacks-signer/src/signerdb.rs @@ -856,22 +856,90 @@ BEGIN END; "#; -/// Migration logic necessary to move blocks from the old blocks table to the new blocks table -/// with the approved_time field added (treated as the signed_self time for existing rows) -/// Drops the signed_over column and associated index, and adds new indexes for querying by approved_time/signed_self/signed_group -static ADD_AND_FILL_APPROVED_TIME: &str = r#" --- Add approved_time column (used to track pre-commit / approval time) -ALTER TABLE blocks - ADD COLUMN approved_time INTEGER; +/// Migration logic to add approved_time and remove signed_over from the blocks table. +/// +/// Uses the recreate-table approach instead of `ALTER TABLE DROP COLUMN` because +/// `DROP COLUMN` can leave the database in a half-migrated state if it fails +/// inside a transaction (the prior `ADD COLUMN` may not roll back cleanly, +/// making the migration non-idempotent on retry). +static MIGRATE_BLOCKS_DROP_SIGNED_OVER_ADD_APPROVED_TIME: &str = r#" +CREATE TABLE IF NOT EXISTS new_blocks ( + signer_signature_hash TEXT NOT NULL PRIMARY KEY, + reward_cycle INTEGER NOT NULL, + block_info TEXT NOT NULL, + consensus_hash TEXT NOT NULL, + broadcasted INTEGER, + stacks_height INTEGER NOT NULL, + burn_block_height INTEGER NOT NULL, + valid INTEGER, + state TEXT NOT NULL, + signed_group INTEGER, + signed_self INTEGER, + proposed_time INTEGER NOT NULL, + validation_time_ms INTEGER, + tenure_change INTEGER NOT NULL, + tenure_change_cause INTEGER, + approved_time INTEGER +) STRICT; --- Backfill approved_time from legacy signed_self timestamps -UPDATE blocks -SET approved_time = signed_self -WHERE approved_time IS NULL - AND signed_self IS NOT NULL; +INSERT OR IGNORE INTO new_blocks ( + signer_signature_hash, + reward_cycle, + block_info, + consensus_hash, + broadcasted, + stacks_height, + burn_block_height, + valid, + state, + signed_group, + signed_self, + proposed_time, + validation_time_ms, + tenure_change, + tenure_change_cause, + approved_time +) +SELECT + signer_signature_hash, + reward_cycle, + block_info, + consensus_hash, + broadcasted, + stacks_height, + burn_block_height, + valid, + state, + signed_group, + signed_self, + proposed_time, + validation_time_ms, + tenure_change, + tenure_change_cause, + signed_self +FROM blocks; + +DROP TABLE blocks; +ALTER TABLE new_blocks RENAME TO blocks; +"#; + +/// Recreate indexes on the blocks table after the table was rebuilt. +/// DROP TABLE removes all indexes, so we must recreate the surviving ones +/// from earlier migrations (INDEXES_5, INDEXES_8) plus the new ones for +/// migration 19. Indexes that referenced `signed_over` are intentionally +/// omitted since that column no longer exists. +static CREATE_INDEXES_19: &str = r#" +-- Surviving indexes from INDEXES_5 +CREATE INDEX IF NOT EXISTS blocks_consensus_hash_state ON blocks (consensus_hash, state); +CREATE INDEX IF NOT EXISTS blocks_state ON blocks (state); +CREATE INDEX IF NOT EXISTS blocks_signed_group ON blocks (signed_group); + +-- Surviving indexes from INDEXES_8 +CREATE INDEX IF NOT EXISTS blocks_consensus_hash_state_height ON blocks (consensus_hash, state, stacks_height DESC); +CREATE INDEX IF NOT EXISTS blocks_state_height_signed_group ON blocks (state, stacks_height DESC, signed_group DESC); +CREATE INDEX IF NOT EXISTS blocks_reward_cycle_state ON blocks (reward_cycle, state); --- Replace the old query optimization index to use approved_time -DROP INDEX IF EXISTS idx_blocks_query_opt; +-- New index replacing idx_blocks_query_opt (now uses approved_time instead of signed_self) CREATE INDEX IF NOT EXISTS idx_blocks_get_last_globally_accepted_block_approved_time ON blocks ( consensus_hash, @@ -880,12 +948,7 @@ ON blocks ( burn_block_height DESC ); --- Remove legacy signed_over plumbing -DROP INDEX IF EXISTS blocks_signed_over; -DROP INDEX IF EXISTS blocks_consensus_hash_status_height; -ALTER TABLE blocks DROP COLUMN signed_over; - --- Add partial indexes for fast tenure-level "has signed block" queries +-- New partial indexes for fast tenure-level queries CREATE INDEX IF NOT EXISTS idx_blocks_tenure_self_signed ON blocks (consensus_hash, stacks_height) WHERE signed_self IS NOT NULL; @@ -1023,7 +1086,8 @@ static SCHEMA_18: &[&str] = &[ ]; static SCHEMA_19: &[&str] = &[ - ADD_AND_FILL_APPROVED_TIME, + MIGRATE_BLOCKS_DROP_SIGNED_OVER_ADD_APPROVED_TIME, + CREATE_INDEXES_19, CREATE_SIGNER_PENDING_PRE_COMMIT_RESPONSES, CREATE_SIGNER_PENDING_SIGNATURE_RESPONSES, CREATE_SIGNER_PENDING_REJECTION_RESPONSES, @@ -1034,92 +1098,126 @@ static SCHEMA_19: &[&str] = &[ ]; struct Migration { - version: u32, + version: SchemaVersion, statements: &'static [&'static str], } +/// Enum representing each schema version. Adding a new schema version requires +/// adding a variant here, a corresponding entry in `MIGRATIONS`, and a test +/// case in `test_all_schema_migrations_have_tests` (which uses an exhaustive +/// match to guarantee compile-time coverage). +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[repr(u32)] +enum SchemaVersion { + V1 = 1, + V2 = 2, + V3 = 3, + V4 = 4, + V5 = 5, + V6 = 6, + V7 = 7, + V8 = 8, + V9 = 9, + V10 = 10, + V11 = 11, + V12 = 12, + V13 = 13, + V14 = 14, + V15 = 15, + V16 = 16, + V17 = 17, + V18 = 18, + V19 = 19, +} + +impl SchemaVersion { + const fn as_u32(self) -> u32 { + self as u32 + } +} + static MIGRATIONS: &[Migration] = &[ Migration { - version: 1, + version: SchemaVersion::V1, statements: SCHEMA_1, }, Migration { - version: 2, + version: SchemaVersion::V2, statements: SCHEMA_2, }, Migration { - version: 3, + version: SchemaVersion::V3, statements: SCHEMA_3, }, Migration { - version: 4, + version: SchemaVersion::V4, statements: SCHEMA_4, }, Migration { - version: 5, + version: SchemaVersion::V5, statements: SCHEMA_5, }, Migration { - version: 6, + version: SchemaVersion::V6, statements: SCHEMA_6, }, Migration { - version: 7, + version: SchemaVersion::V7, statements: SCHEMA_7, }, Migration { - version: 8, + version: SchemaVersion::V8, statements: SCHEMA_8, }, Migration { - version: 9, + version: SchemaVersion::V9, statements: SCHEMA_9, }, Migration { - version: 10, + version: SchemaVersion::V10, statements: SCHEMA_10, }, Migration { - version: 11, + version: SchemaVersion::V11, statements: SCHEMA_11, }, Migration { - version: 12, + version: SchemaVersion::V12, statements: SCHEMA_12, }, Migration { - version: 13, + version: SchemaVersion::V13, statements: SCHEMA_13, }, Migration { - version: 14, + version: SchemaVersion::V14, statements: SCHEMA_14, }, Migration { - version: 15, + version: SchemaVersion::V15, statements: SCHEMA_15, }, Migration { - version: 16, + version: SchemaVersion::V16, statements: SCHEMA_16, }, Migration { - version: 17, + version: SchemaVersion::V17, statements: SCHEMA_17, }, Migration { - version: 18, + version: SchemaVersion::V18, statements: SCHEMA_18, }, Migration { - version: 19, + version: SchemaVersion::V19, statements: SCHEMA_19, }, ]; impl SignerDb { /// The current schema version used in this build of the signer binary. - pub const SCHEMA_VERSION: u32 = 19; + pub const SCHEMA_VERSION: u32 = SchemaVersion::V19.as_u32(); /// Create a new `SignerState` instance. /// This will create a new SQLite database at the given path @@ -1195,34 +1293,31 @@ impl SignerDb { debug!("Current SignerDB schema version: {}", current_db_version); for migration in MIGRATIONS.iter() { - if current_db_version >= migration.version { + let version = migration.version.as_u32(); + if current_db_version >= version { // don't need this migration, continue to see if we need later migrations continue; } - if current_db_version != migration.version - 1 { + if current_db_version != version - 1 { // This implies a gap or out-of-order migration definition, // or the database is at a version X, and the next migration is X+2 instead of X+1. sql_tx.rollback()?; return Err(DBError::Other(format!( "Migration step missing or out of order. Current DB version: {}, trying to apply migration for version: {}", - current_db_version, migration.version + current_db_version, version ))); } - debug!( - "Applying SignerDB migration for schema version {}", - migration.version - ); + debug!("Applying SignerDB migration for schema version {}", version); for statement in migration.statements.iter() { sql_tx.execute_batch(statement)?; } // Verify that the migration script updated the version correctly let new_version_check = Self::get_schema_version(&sql_tx)?; - if new_version_check != migration.version { + if new_version_check != version { sql_tx.rollback()?; return Err(DBError::Other(format!( - "Migration to version {} failed to update DB version. Expected {}, got {new_version_check}.", - migration.version, migration.version + "Migration to version {version} failed to update DB version. Expected {version}, got {new_version_check}." ))); } current_db_version = new_version_check; @@ -4394,4 +4489,240 @@ pub mod tests { "Signer2 should not be in block 2 (only added to blocks 0, 1)" ); } + + /// Run migrations up to (and including) the given version on a raw connection. + /// Caller must register scalar functions beforehand if running early migrations. + /// Insert a block into the schema-5 blocks table using raw SQL. + /// Builds a real `BlockInfo` so the `block_info` JSON is valid for + /// deserialization after migration. Returns the `Sha512Trunc256Sum` + /// so callers can use `block_lookup` to verify data post-migration. + fn insert_schema5_block( + conn: &Connection, + consensus_hash: ConsensusHash, + chain_length: u64, + signed_self: Option, + ) -> Sha512Trunc256Sum { + let (mut block_info, _) = create_block_override(|b| { + b.block.header.consensus_hash = consensus_hash; + b.block.header.chain_length = chain_length; + }); + block_info.valid = Some(true); + block_info.state = BlockState::GloballyAccepted; + block_info.signed_self = signed_self; + + let sighash = block_info.signer_signature_hash(); + let block_json = + serde_json::to_string(&block_info).expect("Unable to serialize block info"); + + conn.execute( + "INSERT INTO blocks ( + signer_signature_hash, reward_cycle, block_info, consensus_hash, + signed_over, broadcasted, stacks_height, burn_block_height, + valid, state, signed_group, signed_self, + proposed_time, validation_time_ms, tenure_change + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)", + params![ + sighash.to_string(), + u64_to_sql(block_info.reward_cycle).unwrap(), + block_json, + block_info.block.header.consensus_hash.to_hex(), + 1i64, // signed_over + None::, // broadcasted + u64_to_sql(block_info.block.header.chain_length).unwrap(), + u64_to_sql(block_info.burn_block_height).unwrap(), + &block_info.valid, + &block_info.state.to_string(), + &block_info.signed_group, + &block_info.signed_self, + u64_to_sql(block_info.proposed_time).unwrap(), + &block_info.validation_time_ms, + &block_info.is_tenure_change(), + ], + ) + .unwrap(); + + sighash + } + + /// Progressively applies every migration one at a time, running + /// per-version validations at each step. The exhaustive `match` means + /// adding a new `SchemaVersion` variant without handling it here will + /// cause a compile error. + /// + /// When adding a new migration: + /// 1. Add the `SchemaVersion` variant + /// 2. Add the `Migration` entry in `MIGRATIONS` + /// 3. Add the variant to the match below with version-specific checks + #[test] + fn test_all_schema_migrations() { + let db_path = tmp_db_path(); + let mut signer_db = SignerDb { + db: SignerDb::connect(&db_path).unwrap(), + }; + signer_db.register_scalar_functions().unwrap(); + + let mut hash_signed = Sha512Trunc256Sum([0; 32]); + let mut hash_unsigned = Sha512Trunc256Sum([0; 32]); + + for migration in MIGRATIONS { + // Apply this single migration + let tx = tx_begin_immediate(&mut signer_db.db).unwrap(); + for statement in migration.statements { + tx.execute_batch(statement).unwrap(); + } + tx.commit().unwrap(); + + let version = migration.version.as_u32(); + assert_eq!( + SignerDb::get_schema_version(&signer_db.db).unwrap(), + version, + "Migration to version {version} did not set the correct schema version" + ); + + // Exhaustive match: per-version setup and validation. + // Adding a new SchemaVersion variant without a branch here + // will fail to compile. + match migration.version { + SchemaVersion::V1 | SchemaVersion::V2 | SchemaVersion::V3 | SchemaVersion::V4 => {} + SchemaVersion::V5 => { + // Schema 5 is the first restructured blocks table. + // Insert test data that must survive all subsequent migrations. + hash_signed = insert_schema5_block( + &signer_db.db, + ConsensusHash([0x01; 20]), + 100, + Some(1000), + ); + hash_unsigned = + insert_schema5_block(&signer_db.db, ConsensusHash([0x02; 20]), 101, None); + } + SchemaVersion::V6 + | SchemaVersion::V7 + | SchemaVersion::V8 + | SchemaVersion::V9 + | SchemaVersion::V10 + | SchemaVersion::V11 + | SchemaVersion::V12 + | SchemaVersion::V13 + | SchemaVersion::V14 + | SchemaVersion::V15 + | SchemaVersion::V16 + | SchemaVersion::V17 => {} + SchemaVersion::V18 => { + // signed_over column should still exist before V19 removes it + let signed_over: i64 = signer_db + .db + .query_row( + &format!( + "SELECT signed_over FROM blocks WHERE signer_signature_hash = '{hash_signed}'" + ), + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(signed_over, 1); + } + SchemaVersion::V19 => { + // signed_over column should be removed + assert!( + signer_db + .db + .execute("SELECT signed_over FROM blocks LIMIT 1", []) + .is_err(), + "signed_over column should not exist after V19" + ); + + // approved_time backfilled from signed_self + let approved_time: Option = signer_db.db.query_row( + &format!("SELECT approved_time FROM blocks WHERE signer_signature_hash = '{hash_signed}'"), + [], |row| row.get(0), + ).unwrap(); + assert_eq!(approved_time, Some(1000)); + + let approved_time: Option = signer_db.db.query_row( + &format!("SELECT approved_time FROM blocks WHERE signer_signature_hash = '{hash_unsigned}'"), + [], |row| row.get(0), + ).unwrap(); + assert!(approved_time.is_none()); + + // Verify indexes survived the table rebuild + let index_names: Vec = signer_db + .db + .prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'blocks'") + .unwrap() + .query_map([], |row| row.get(0)) + .unwrap() + .collect::>() + .unwrap(); + for expected in &[ + "blocks_consensus_hash_state", + "blocks_state", + "blocks_signed_group", + "blocks_consensus_hash_state_height", + "blocks_state_height_signed_group", + "blocks_reward_cycle_state", + "idx_blocks_get_last_globally_accepted_block_approved_time", + "idx_blocks_tenure_self_signed", + "idx_blocks_tenure_group_signed", + "idx_blocks_tenure_approved", + ] { + assert!( + index_names.contains(&expected.to_string()), + "Missing index: {expected}" + ); + } + for removed in &[ + "blocks_signed_over", + "blocks_consensus_hash_status_height", + "idx_blocks_query_opt", + ] { + assert!( + !index_names.contains(&removed.to_string()), + "Index should not exist: {removed}" + ); + } + } + } + } + + // Verify data survived all migrations + let block_signed = signer_db + .block_lookup(&hash_signed) + .unwrap() + .expect("Block with signed_self should exist after all migrations"); + assert_eq!(block_signed.block.header.chain_length, 100); + assert_eq!(block_signed.signed_self, Some(1000)); + assert_eq!(block_signed.state, BlockState::GloballyAccepted); + + let block_unsigned = signer_db + .block_lookup(&hash_unsigned) + .unwrap() + .expect("Block without signed_self should exist after all migrations"); + assert_eq!(block_unsigned.block.header.chain_length, 101); + assert!(block_unsigned.signed_self.is_none()); + + // Database is usable: insert and read back a new block + let (block_info, block_proposal) = create_block(); + signer_db.insert_block(&block_info).unwrap(); + let retrieved = signer_db + .block_lookup(&block_proposal.block.header.signer_signature_hash()) + .unwrap() + .expect("Should retrieve inserted block"); + assert_eq!(BlockInfo::from(block_proposal), retrieved); + + // Reopening is idempotent + signer_db.remove_scalar_functions().unwrap(); + drop(signer_db); + let db = SignerDb::new(&db_path).expect("Re-opening should succeed"); + assert_eq!( + SignerDb::get_schema_version(&db.db).unwrap(), + SignerDb::SCHEMA_VERSION + ); + + assert_eq!( + MIGRATIONS.last().unwrap().version.as_u32(), + SignerDb::SCHEMA_VERSION, + "Last migration version must match SCHEMA_VERSION" + ); + } } diff --git a/stackslib/src/chainstate/burn/db/sortdb.rs b/stackslib/src/chainstate/burn/db/sortdb.rs index 79d5f82c9be..3125db5630b 100644 --- a/stackslib/src/chainstate/burn/db/sortdb.rs +++ b/stackslib/src/chainstate/burn/db/sortdb.rs @@ -69,7 +69,7 @@ const BLOCK_HEIGHT_MAX: u64 = (1 << 63) - 1; pub const REWARD_WINDOW_START: u64 = 144 * 15; pub const REWARD_WINDOW_END: u64 = 144 * 90 + REWARD_WINDOW_START; -pub const STACKS_TIPS_BY_BURN_VIEW_SEARCH_DEPTH: usize = 144; +pub const STACKS_TIPS_BY_BURN_VIEW_SEARCH_DEPTH: usize = 4200; pub type BlockHeaderCache = HashMap, ConsensusHash)>; diff --git a/stackslib/src/chainstate/coordinator/tests.rs b/stackslib/src/chainstate/coordinator/tests.rs index fa49da06df1..51bddba2d93 100644 --- a/stackslib/src/chainstate/coordinator/tests.rs +++ b/stackslib/src/chainstate/coordinator/tests.rs @@ -1280,7 +1280,7 @@ fn missed_block_commits_2_05() { PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| env.eval_raw("block-height") + |exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, "block-height") ) .unwrap() ) @@ -1629,7 +1629,7 @@ fn missed_block_commits_2_1() { PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| env.eval_raw("block-height") + |exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, "block-height") ) .unwrap() ) @@ -1976,7 +1976,7 @@ fn late_block_commits_2_1() { PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| env.eval_raw("block-height") + |exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, "block-height") ) .unwrap() ) @@ -2150,7 +2150,7 @@ fn test_simple_setup() { PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| env.eval_raw("block-height") + |exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, "block-height") ) .unwrap() ) @@ -2453,7 +2453,7 @@ fn test_sortition_with_reward_set() { PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| env.eval_raw("block-height") + |exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, "block-height") ) .unwrap() ) @@ -2695,7 +2695,7 @@ fn test_sortition_with_burner_reward_set() { PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| env.eval_raw("block-height") + |exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, "block-height") ) .unwrap() ) @@ -2983,7 +2983,7 @@ fn test_pox_btc_ops() { PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| env.eval_raw("block-height") + |exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, "block-height") ) .unwrap() ) @@ -3326,7 +3326,7 @@ fn test_stx_transfer_btc_ops() { PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| env.eval_raw("block-height") + |exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, "block-height") ) .unwrap() ) @@ -3363,14 +3363,14 @@ fn get_delegation_info_pox_2( PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| { + |exec_state, invoke_ctx| { let eval_str = format!( "(contract-call? '{}.pox-2 get-delegation-info '{})", &boot_code_addr(false), del_addr ); - let result = env.eval_raw(&eval_str).unwrap(); + let result = exec_state.eval_raw(invoke_ctx, &eval_str).unwrap(); Ok(result) }, ) @@ -3722,7 +3722,7 @@ fn test_delegate_stx_btc_ops() { PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| env.eval_raw("block-height") + |exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, "block-height") ) .unwrap() ) @@ -3965,7 +3965,7 @@ fn test_initial_coinbase_reward_distributions() { PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| env.eval_raw("block-height") + |exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, "block-height") ) .unwrap() ) @@ -4880,7 +4880,7 @@ fn get_total_stacked_info( PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| { + |exec_state, invoke_ctx| { let eval_str = format!( "(contract-call? '{}.{} get-total-ustx-stacked u{})", &boot_code_addr(false), @@ -4888,7 +4888,9 @@ fn get_total_stacked_info( reward_cycle ); - let result = env.eval_raw(&eval_str).map(|v| v.expect_u128().unwrap()); + let result = exec_state + .eval_raw(invoke_ctx, &eval_str) + .map(|v| v.expect_u128().unwrap()); Ok(result) }, ) @@ -5491,7 +5493,7 @@ fn test_sortition_with_sunset() { PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| env.eval_raw("block-height") + |exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, "block-height") ) .unwrap() ) @@ -5839,7 +5841,7 @@ fn test_sortition_with_sunset_and_epoch_switch() { PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| env.eval_raw("block-height") + |exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, "block-height") ) .unwrap() ) @@ -6397,7 +6399,7 @@ fn eval_at_chain_tip(chainstate_path: &str, sort_db: &SortitionDB, eval: &str) - PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| env.eval_raw(eval), + |exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, eval), ) .unwrap() }, diff --git a/stackslib/src/chainstate/nakamoto/coordinator/tests.rs b/stackslib/src/chainstate/nakamoto/coordinator/tests.rs index fc976fc68d9..c62d5414a33 100644 --- a/stackslib/src/chainstate/nakamoto/coordinator/tests.rs +++ b/stackslib/src/chainstate/nakamoto/coordinator/tests.rs @@ -3400,7 +3400,7 @@ pub fn simple_nakamoto_coordinator_sip034_tenure_extensions( &format!("test-{contract_count}"), smart_contract, &private_key, - ClarityVersion::latest(), + ClarityVersion::Clarity4, account.nonce, u64::try_from(smart_contract.len() * 2).unwrap(), ); diff --git a/stackslib/src/chainstate/nakamoto/signer_set.rs b/stackslib/src/chainstate/nakamoto/signer_set.rs index 8c78784fa7f..fe9c0b35a46 100644 --- a/stackslib/src/chainstate/nakamoto/signer_set.rs +++ b/stackslib/src/chainstate/nakamoto/signer_set.rs @@ -307,14 +307,16 @@ impl NakamotoSigners { let (value, _, events, _) = clarity.with_abort_callback( |vm_env| { - vm_env.execute_in_env(sender_addr.clone(), None, None, |env| { - env.execute_contract_allow_private( + vm_env.execute_in_env(sender_addr.clone(), None, None, |exec_state, invoke_ctx| { + exec_state.execute_contract_allow_private( + invoke_ctx, signers_contract, "stackerdb-set-signer-slots", &set_stackerdb_args, false, )?; - env.execute_contract_allow_private( + exec_state.execute_contract_allow_private( + invoke_ctx, signers_contract, "set-signers", &set_signers_args, diff --git a/stackslib/src/chainstate/stacks/block.rs b/stackslib/src/chainstate/stacks/block.rs index f0f81302578..d8093e155df 100644 --- a/stackslib/src/chainstate/stacks/block.rs +++ b/stackslib/src/chainstate/stacks/block.rs @@ -273,7 +273,12 @@ impl StacksBlockHeader { }; if !valid { - let msg = format!("Invalid Stacks block header {}: leader VRF key {} did not produce a valid proof over {}", self.block_hash(), leader_key.public_key.to_hex(), burn_chain_tip.sortition_hash); + let msg = format!( + "Invalid Stacks block header {}: leader VRF key {} did not produce a valid proof over {}", + self.block_hash(), + leader_key.public_key.to_hex(), + burn_chain_tip.sortition_hash + ); warn!("{}", msg); return Err(Error::InvalidStacksBlock(msg)); } @@ -568,6 +573,24 @@ impl StacksBlock { tx: &StacksTransaction, epoch_id: StacksEpochId, ) -> bool { + if tx.post_condition_mode == TransactionPostConditionMode::Originator + && !epoch_id.supports_sip040_post_conditions() + { + error!("Originator post-condition mode is not supported in epoch {epoch_id}"; "txid" => %tx.txid()); + return false; + } + if !epoch_id.supports_sip040_post_conditions() { + for post_condition in tx.post_conditions.iter() { + if let TransactionPostCondition::Nonfungible(_, _, _, condition_code) = + post_condition + { + if *condition_code == NonfungibleConditionCode::MaybeSent { + error!("NFT MaybeSent post-condition is not supported in epoch {epoch_id}"; "txid" => %tx.txid()); + return false; + } + } + } + } if let TransactionPayload::Coinbase(_, ref recipient_opt, ref proof_opt) = &tx.payload { if proof_opt.is_some() && epoch_id < StacksEpochId::Epoch30 { // not supported @@ -936,6 +959,7 @@ impl StacksMicroblock { #[cfg(test)] mod test { use clarity::types::PublicKey; + use rstest::rstest; use stacks_common::address::*; use stacks_common::types::chainstate::StacksAddress; use stacks_common::util::hash::*; @@ -2114,6 +2138,87 @@ mod test { )); } + #[rstest] + #[case(StacksEpochId::Epoch30, false)] + #[case(StacksEpochId::Epoch31, false)] + #[case(StacksEpochId::Epoch32, false)] + #[case(StacksEpochId::Epoch33, false)] + #[case(StacksEpochId::Epoch34, true)] + fn test_validate_transaction_static_epoch_originator_mode_gated_to_epoch34( + #[case] epoch_id: StacksEpochId, + #[case] expected: bool, + ) { + let privk = StacksPrivateKey::random(); + let origin_auth = TransactionAuth::Standard( + TransactionSpendingCondition::new_singlesig_p2pkh(StacksPublicKey::from_private( + &privk, + )) + .unwrap(), + ); + + let mut tx = StacksTransaction::new( + TransactionVersion::Testnet, + origin_auth, + TransactionPayload::TokenTransfer( + PrincipalData::from(StacksAddress::new(1, Hash160([0x11; 20])).unwrap()), + 123, + TokenTransferMemo([0u8; 34]), + ), + ); + tx.post_condition_mode = TransactionPostConditionMode::Originator; + + assert_eq!( + StacksBlock::validate_transaction_static_epoch(&tx, epoch_id), + expected + ); + } + + #[rstest] + #[case(StacksEpochId::Epoch30, false)] + #[case(StacksEpochId::Epoch31, false)] + #[case(StacksEpochId::Epoch32, false)] + #[case(StacksEpochId::Epoch33, false)] + #[case(StacksEpochId::Epoch34, true)] + fn test_validate_transaction_static_epoch_nft_maybesent_gated_to_epoch34( + #[case] epoch_id: StacksEpochId, + #[case] expected: bool, + ) { + let privk = StacksPrivateKey::random(); + let origin_auth = TransactionAuth::Standard( + TransactionSpendingCondition::new_singlesig_p2pkh(StacksPublicKey::from_private( + &privk, + )) + .unwrap(), + ); + + let mut tx = StacksTransaction::new( + TransactionVersion::Testnet, + origin_auth, + TransactionPayload::TokenTransfer( + PrincipalData::from(StacksAddress::new(1, Hash160([0x11; 20])).unwrap()), + 123, + TokenTransferMemo([0u8; 34]), + ), + ); + + tx.post_conditions + .push(TransactionPostCondition::Nonfungible( + PostConditionPrincipal::Origin, + AssetInfo { + contract_address: StacksAddress::new(1, Hash160([0x22; 20])).unwrap(), + contract_name: ContractName::try_from("hello-world").unwrap(), + asset_name: ClarityName::try_from("asset").unwrap(), + }, + Value::Int(1), + NonfungibleConditionCode::MaybeSent, + )); + + assert_eq!( + StacksBlock::validate_transaction_static_epoch(&tx, epoch_id), + expected + ); + } + // TODO: // * size limits } diff --git a/stackslib/src/chainstate/stacks/boot/contract_tests.rs b/stackslib/src/chainstate/stacks/boot/contract_tests.rs index efd28bddd2a..6049f649471 100644 --- a/stackslib/src/chainstate/stacks/boot/contract_tests.rs +++ b/stackslib/src/chainstate/stacks/boot/contract_tests.rs @@ -1,3 +1,17 @@ +// Copyright (C) 2020-2026 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . use std::ops::Deref; use clarity::util::get_epoch_time_secs; @@ -891,20 +905,26 @@ fn pox_2_lock_extend_units() { None, ) .unwrap(); - env.execute_in_env(boot_code_addr(false).into(), None, None, |env| { - env.execute_contract( - POX_2_CONTRACT_TESTNET.deref(), - "set-burnchain-parameters", - &symbols_from_values(vec![ - Value::UInt(0), - Value::UInt(1), - Value::UInt(reward_cycle_len), - Value::UInt(25), - Value::UInt(0), - ]), - false, - ) - }) + env.execute_in_env( + boot_code_addr(false).into(), + None, + None, + |exec_state, invoke_ctx| { + exec_state.execute_contract( + invoke_ctx, + POX_2_CONTRACT_TESTNET.deref(), + "set-burnchain-parameters", + &symbols_from_values(vec![ + Value::UInt(0), + Value::UInt(1), + Value::UInt(reward_cycle_len), + Value::UInt(25), + Value::UInt(0), + ]), + false, + ) + }, + ) .unwrap(); }); diff --git a/stackslib/src/chainstate/stacks/boot/mod.rs b/stackslib/src/chainstate/stacks/boot/mod.rs index 5f0856a644a..2348daeacb7 100644 --- a/stackslib/src/chainstate/stacks/boot/mod.rs +++ b/stackslib/src/chainstate/stacks/boot/mod.rs @@ -21,6 +21,7 @@ use std::sync::LazyLock; use clarity::types::Address; use clarity::vm::analysis::RuntimeCheckErrorKind; use clarity::vm::clarity::{ClarityError, TransactionConnection}; +use clarity::vm::contexts::ExecutionState; use clarity::vm::costs::LimitedCostTracker; use clarity::vm::database::{ClarityDatabase, NULL_BURN_STATE_DB, NULL_HEADER_DB}; use clarity::vm::errors::{ClarityEvalError, VmExecutionError}; @@ -29,7 +30,7 @@ use clarity::vm::representations::ContractName; use clarity::vm::types::{ PrincipalData, QualifiedContractIdentifier, StandardPrincipalData, TupleData, Value, }; -use clarity::vm::{Environment, SymbolicExpression}; +use clarity::vm::SymbolicExpression; use lazy_static::lazy_static; use serde::Deserialize; use stacks_common::codec::StacksMessageCodec; @@ -389,8 +390,9 @@ impl StacksChainState { sender_addr, None, LimitedCostTracker::new_free(), - |vm_env| { - vm_env.eval_read_only( + |exec_state, invoke_ctx| { + exec_state.eval_read_only( + invoke_ctx, &pox_contract, &format!(r#" (unwrap-panic (map-get? stacking-state {{ stacker: '{unlocked_principal} }})) @@ -437,8 +439,9 @@ impl StacksChainState { sender_addr, None, LimitedCostTracker::new_free(), - |vm_env| { - vm_env.eval_read_only( + |exec_state, invoke_ctx| { + exec_state.eval_read_only( + invoke_ctx, &pox_contract, &format!( r#" @@ -583,20 +586,26 @@ impl StacksChainState { let (result, _, mut events, _) = clarity .with_abort_callback( |vm_env| { - vm_env.execute_in_env(sender_addr.clone(), None, None, |env| { - env.execute_contract_allow_private( - &pox_contract, - "handle-unlock", - &[ - SymbolicExpression::atom_value(principal.clone().into()), - SymbolicExpression::atom_value(Value::UInt(*amount_locked)), - SymbolicExpression::atom_value(Value::UInt( - cycle_number.into(), - )), - ], - false, - ) - }) + vm_env.execute_in_env( + sender_addr.clone(), + None, + None, + |exec_state, invoke_ctx| { + exec_state.execute_contract_allow_private( + invoke_ctx, + &pox_contract, + "handle-unlock", + &[ + SymbolicExpression::atom_value(principal.clone().into()), + SymbolicExpression::atom_value(Value::UInt(*amount_locked)), + SymbolicExpression::atom_value(Value::UInt( + cycle_number.into(), + )), + ], + false, + ) + }, + ) }, |_, _| None, ) @@ -613,7 +622,7 @@ impl StacksChainState { // Add synthetic print event for `handle-unlock`, since it alters stacking state let tx_event = - Environment::construct_print_transaction_event(&pox_contract, &event_info); + ExecutionState::construct_print_transaction_event(pox_contract.clone(), event_info); events.push(tx_event); total_events.extend(events.into_iter()); } @@ -695,14 +704,16 @@ impl StacksChainState { sender, None, cost_track, - |env| { - env.execute_contract( - &contract_identifier, - function, - &[SymbolicExpression::atom_value(Value::UInt(reward_cycle))], - true, - ) - .map_err(ClarityEvalError::from) + |exec_state, invoke_ctx| { + exec_state + .execute_contract( + invoke_ctx, + &contract_identifier, + function, + &[SymbolicExpression::atom_value(Value::UInt(reward_cycle))], + true, + ) + .map_err(ClarityEvalError::from) }, ) }, diff --git a/stackslib/src/chainstate/stacks/boot/pox_2_tests.rs b/stackslib/src/chainstate/stacks/boot/pox_2_tests.rs index 2749bbbf1f2..14b32e9bc66 100644 --- a/stackslib/src/chainstate/stacks/boot/pox_2_tests.rs +++ b/stackslib/src/chainstate/stacks/boot/pox_2_tests.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -3852,8 +3852,9 @@ fn test_get_pox_addrs() { PrincipalData::Standard(StandardPrincipalData::transient()), None, LimitedCostTracker::new_free(), - |env| { - env.eval_read_only( + |exec_state, invoke_ctx| { + exec_state.eval_read_only( + invoke_ctx, &boot_code_id("pox-2", false), &format!( "(get-burn-block-info? pox-addrs u{})", @@ -4150,8 +4151,9 @@ fn test_stack_with_segwit() { PrincipalData::Standard(StandardPrincipalData::transient()), None, LimitedCostTracker::new_free(), - |env| { - env.eval_read_only( + |exec_state, invoke_ctx| { + exec_state.eval_read_only( + invoke_ctx, &boot_code_id("pox-2", false), &format!( "(get-burn-block-info? pox-addrs u{})", diff --git a/stackslib/src/chainstate/stacks/boot/pox_3_tests.rs b/stackslib/src/chainstate/stacks/boot/pox_3_tests.rs index a99d4c98c29..f22c1f558da 100644 --- a/stackslib/src/chainstate/stacks/boot/pox_3_tests.rs +++ b/stackslib/src/chainstate/stacks/boot/pox_3_tests.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -3330,8 +3330,9 @@ fn get_burn_pox_addr_info(peer: &mut TestPeer) -> (Vec, u128) { PrincipalData::Standard(StandardPrincipalData::transient()), None, LimitedCostTracker::new_free(), - |env| { - env.eval_read_only( + |exec_state, invoke_ctx| { + exec_state.eval_read_only( + invoke_ctx, &boot_code_id("pox-2", false), &format!("(get-burn-block-info? pox-addrs u{})", &burn_height), ) diff --git a/stackslib/src/chainstate/stacks/boot/pox_4_tests.rs b/stackslib/src/chainstate/stacks/boot/pox_4_tests.rs index 90ac2d21e5c..5bb69daae18 100644 --- a/stackslib/src/chainstate/stacks/boot/pox_4_tests.rs +++ b/stackslib/src/chainstate/stacks/boot/pox_4_tests.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -840,8 +840,9 @@ fn get_burn_pox_addr_info(peer: &mut TestPeer) -> (Vec, u128) { PrincipalData::Standard(StandardPrincipalData::transient()), None, LimitedCostTracker::new_free(), - |env| { - env.eval_read_only( + |exec_state, invoke_ctx| { + exec_state.eval_read_only( + invoke_ctx, &boot_code_id("pox-2", false), &format!("(get-burn-block-info? pox-addrs u{})", &burn_height), ) @@ -2966,7 +2967,7 @@ fn verify_signer_key_sig( PrincipalData::Standard(StandardPrincipalData::transient()), None, LimitedCostTracker::new_free(), - |env| { + |exec_state, invoke_ctx| { let program = format!( "(verify-signer-key-sig {} u{} \"{}\" u{} (some 0x{}) 0x{} u{} u{} u{})", Value::Tuple(pox_addr.clone().as_clarity_tuple().unwrap()), @@ -2979,7 +2980,7 @@ fn verify_signer_key_sig( max_amount, auth_id ); - env.eval_read_only(&boot_code_id("pox-4", false), &program) + exec_state.eval_read_only(invoke_ctx, &boot_code_id("pox-4", false), &program) }, ) .unwrap() diff --git a/stackslib/src/chainstate/stacks/boot/signers_tests.rs b/stackslib/src/chainstate/stacks/boot/signers_tests.rs index b8126614ea7..5910e17feda 100644 --- a/stackslib/src/chainstate/stacks/boot/signers_tests.rs +++ b/stackslib/src/chainstate/stacks/boot/signers_tests.rs @@ -473,14 +473,16 @@ pub fn readonly_call_with_sortdb( PrincipalData::from(boot_code_addr(false)), None, LimitedCostTracker::new_free(), - |env| { - env.execute_contract_allow_private( - &boot_code_id(&boot_contract, false), - &function_name, - &symbols_from_values(args), - true, - ) - .map_err(ClarityEvalError::from) + |exec_state, invoke_ctx| { + exec_state + .execute_contract_allow_private( + &invoke_ctx, + &boot_code_id(&boot_contract, false), + &function_name, + &symbols_from_values(args), + true, + ) + .map_err(ClarityEvalError::from) }, ) .unwrap() diff --git a/stackslib/src/chainstate/stacks/db/mod.rs b/stackslib/src/chainstate/stacks/db/mod.rs index 35636d29282..7d5c094c19b 100644 --- a/stackslib/src/chainstate/stacks/db/mod.rs +++ b/stackslib/src/chainstate/stacks/db/mod.rs @@ -2069,14 +2069,15 @@ impl StacksChainState { contract.clone().into(), None, LimitedCostTracker::Free, - |env| { - env.execute_contract( - contract, function, &args, - // read-only is set to `false` so that non-read-only functions - // can be executed. any transformation is rolled back. - false, - ) - .map_err(ClarityEvalError::from) + |exec_state, invoke_ctx| { + exec_state + .execute_contract( + invoke_ctx, contract, function, &args, + // read-only is set to `false` so that non-read-only functions + // can be executed. any transformation is rolled back. + false, + ) + .map_err(ClarityEvalError::from) }, )?; diff --git a/stackslib/src/chainstate/stacks/db/transactions.rs b/stackslib/src/chainstate/stacks/db/transactions.rs index 9ec066689a8..ae5796f3c0a 100644 --- a/stackslib/src/chainstate/stacks/db/transactions.rs +++ b/stackslib/src/chainstate/stacks/db/transactions.rs @@ -18,7 +18,7 @@ use std::collections::{HashMap, HashSet}; use clarity::vm::analysis::types::ContractAnalysis; use clarity::vm::clarity::TransactionConnection; -use clarity::vm::contexts::{AssetMap, AssetMapEntry, Environment}; +use clarity::vm::contexts::{AssetMap, AssetMapEntry, ExecutionState, InvocationContext}; use clarity::vm::costs::cost_functions::ClarityCostFunction; use clarity::vm::costs::{runtime_cost, CostTracker, ExecutionCost}; use clarity::vm::errors::{VmExecutionError, VmInternalError}; @@ -617,6 +617,29 @@ impl StacksChainState { } } + // check if post-condition mode is supported in this epoch + if tx.post_condition_mode == TransactionPostConditionMode::Originator + && !epoch_id.supports_sip040_post_conditions() + { + let msg = "Invalid Stacks transaction: Originator post-condition mode is not supported before Stacks 3.4".to_string(); + info!("{}", &msg; "txid" => %tx.txid()); + return Err(Error::InvalidStacksTransaction(msg, false)); + } + // check if MaybeSent NFT post-conditions are supported in this epoch + if !epoch_id.supports_sip040_post_conditions() { + for post_condition in tx.post_conditions.iter() { + if let TransactionPostCondition::Nonfungible(_, _, _, condition_code) = + post_condition + { + if *condition_code == NonfungibleConditionCode::MaybeSent { + let msg = "Invalid Stacks transaction: NFT MaybeSent post-condition is not supported before Stacks 3.4".to_string(); + info!("{}", &msg; "txid" => %tx.txid()); + return Err(Error::InvalidStacksTransaction(msg, false)); + } + } + } + } + Ok(()) } @@ -637,7 +660,12 @@ impl StacksChainState { PrincipalData, HashMap>, > = HashMap::new(); - let allow_unchecked_assets = *post_condition_mode == TransactionPostConditionMode::Allow; + let enforce_unchecked_assets_for_principal = + |principal: &PrincipalData| match post_condition_mode { + TransactionPostConditionMode::Allow => false, + TransactionPostConditionMode::Deny => true, + TransactionPostConditionMode::Originator => principal == &origin_account.principal, + }; for postcond in post_conditions { match postcond { @@ -702,7 +730,9 @@ impl StacksChainState { .get_fungible_tokens(&account_principal, &asset_id) .unwrap_or(0); if !condition_code.check(u128::from(*amount_sent_condition), amount_sent) { - let reason = format!("Post-condition check failure on fungible asset {asset_id} owned by {account_principal}: {amount_sent_condition} {condition_code:?} {amount_sent}"); + let reason = format!( + "Post-condition check failure on fungible asset {asset_id} owned by {account_principal}: {amount_sent_condition} {condition_code:?} {amount_sent}" + ); info!("{reason}"; "txid" => %txid); return Ok(Some(reason)); } @@ -765,65 +795,66 @@ impl StacksChainState { } } - if !allow_unchecked_assets { - // make sure every asset transferred is covered by a postcondition - let asset_map_copy = (*asset_map).clone(); - let mut all_assets_sent = asset_map_copy.to_table(); - for (principal, mut assets) in all_assets_sent.drain() { - for (asset_identifier, asset_entry) in assets.drain() { - match asset_entry { - AssetMapEntry::Asset(values) => { - // this is a NFT - if let Some(checked_nft_asset_map) = - checked_nonfungible_assets.get(&principal) - { - if let Some(nfts) = checked_nft_asset_map.get(&asset_identifier) { - // each value must be covered - for v in values { - if !nfts.contains(&v.clone().try_into()?) { - let reason = format!( - "Post-condition check failure: Non-fungible asset {asset_identifier} value {v:?} was moved by {principal} but not checked" - ); - info!("{reason}"; "txid" => %txid); - return Ok(Some(reason)); - } + // make sure every asset transferred is covered by a postcondition, if the current mode + // requires it. + let asset_map_copy = (*asset_map).clone(); + let mut all_assets_sent = asset_map_copy.to_table(); + for (principal, mut assets) in all_assets_sent.drain() { + if !enforce_unchecked_assets_for_principal(&principal) { + continue; + } + for (asset_identifier, asset_entry) in assets.drain() { + match asset_entry { + AssetMapEntry::Asset(values) => { + // this is a NFT + if let Some(checked_nft_asset_map) = + checked_nonfungible_assets.get(&principal) + { + if let Some(nfts) = checked_nft_asset_map.get(&asset_identifier) { + // each value must be covered + for v in values { + if !nfts.contains(&v.clone().try_into()?) { + let reason = format!( + "Post-condition check failure: Non-fungible asset {asset_identifier} value {v:?} was moved by {principal} but not checked" + ); + info!("{reason}"; "txid" => %txid); + return Ok(Some(reason)); } - } else { - // no values covered - let reason = format!( - "Post-condition check failure: Non-fungible asset {asset_identifier} was moved by {principal} but not checked" - ); - info!("{reason}"; "txid" => %txid); - return Ok(Some(reason)); } } else { - // no NFT for this principal + // no values covered let reason = format!( - "Post-condition check failure: No checks for non-fungible asset {asset_identifier} moved by {principal}" + "Post-condition check failure: Non-fungible asset {asset_identifier} was moved by {principal} but not checked" ); info!("{reason}"; "txid" => %txid); return Ok(Some(reason)); } + } else { + // no NFT for this principal + let reason = format!( + "Post-condition check failure: No checks for non-fungible asset {asset_identifier} moved by {principal}" + ); + info!("{reason}"; "txid" => %txid); + return Ok(Some(reason)); } - _ => { - // This is STX or a fungible token - if let Some(checked_ft_asset_ids) = - checked_fungible_assets.get(&principal) - { - if !checked_ft_asset_ids.contains(&asset_identifier) { - let reason = format!( - "Post-condition check failure: Fungible asset {asset_identifier} was moved by {principal} but not checked" - ); - info!("{reason}"; "txid" => %txid); - return Ok(Some(reason)); - } - } else { + } + _ => { + // This is STX or a fungible token + if let Some(checked_ft_asset_ids) = checked_fungible_assets.get(&principal) + { + if !checked_ft_asset_ids.contains(&asset_identifier) { let reason = format!( "Post-condition check failure: Fungible asset {asset_identifier} was moved by {principal} but not checked" ); info!("{reason}"; "txid" => %txid); return Ok(Some(reason)); } + } else { + let reason = format!( + "Post-condition check failure: Fungible asset {asset_identifier} was moved by {principal} but not checked" + ); + info!("{reason}"; "txid" => %txid); + return Ok(Some(reason)); } } } @@ -871,7 +902,8 @@ impl StacksChainState { /// * contains the sender that reported the poison-microblock /// * contains the sequence number at which the fork occurred pub fn handle_poison_microblock( - env: &mut Environment, + env: &mut ExecutionState, + invoke_ctx: &InvocationContext, mblock_header_1: &StacksMicroblockHeader, mblock_header_2: &StacksMicroblockHeader, ) -> Result { @@ -882,7 +914,7 @@ impl StacksChainState { runtime_cost(ClarityCostFunction::PoisonMicroblock, env, 0) .map_err(|e| Error::from_cost_error(e, cost_before.clone(), env.global_context))?; - let sender_principal = match &env.sender { + let sender_principal = match &invoke_ctx.sender { Some(ref sender) => { if let PrincipalData::Standard(sender) = sender.clone() { sender @@ -938,7 +970,10 @@ impl StacksChainState { .expect("BUG: too many blocks") < current_height { - let msg = format!("Invalid Stacks transaction: microblock public key hash from height {} has matured relative to current height {}", height, current_height); + let msg = format!( + "Invalid Stacks transaction: microblock public key hash from height {} has matured relative to current height {}", + height, current_height + ); warn!("{}", &msg; "microblock_pubkey_hash" => %pubkh ); @@ -1276,7 +1311,13 @@ impl StacksChainState { Err(e) => { match e { ClarityError::CostError(ref cost_after, ref budget) => { - warn!("Block compute budget exceeded on {}: cost before={}, after={}, budget={}", tx.txid(), &cost_before, cost_after, budget); + warn!( + "Block compute budget exceeded on {}: cost before={}, after={}, budget={}", + tx.txid(), + &cost_before, + cost_after, + budget + ); return Err(Error::CostOverflowError( cost_before, cost_after.clone(), @@ -1286,13 +1327,19 @@ impl StacksChainState { other_error => { if let ClarityError::Parse(err) = &other_error { if err.rejectable_in_epoch(clarity_tx.get_epoch()) { - info!("Transaction {} is problematic and should have prevented this block from being relayed", tx.txid()); + info!( + "Transaction {} is problematic and should have prevented this block from being relayed", + tx.txid() + ); return Err(Error::ClarityError(other_error)); } } if let ClarityError::StaticCheck(err) = &other_error { if err.err.rejectable_in_epoch(clarity_tx.get_epoch()) { - info!("Transaction {} is problematic and should have prevented this block from being relayed", tx.txid()); + info!( + "Transaction {} is problematic and should have prevented this block from being relayed", + tx.txid() + ); return Err(Error::ClarityError(other_error)); } } @@ -1511,8 +1558,7 @@ impl StacksChainState { { let msg = format!( "Invalid Stacks transaction: TenureChange cause variant {:?} is not supported in epoch {:?}", - &payload.cause, - &epoch_id + &payload.cause, &epoch_id ); info!("{msg}"); return Err(Error::InvalidStacksTransaction(msg, false)); @@ -1668,7 +1714,9 @@ pub mod test { use clarity::vm::test_util::{UnitTestBurnStateDB, TEST_BURN_STATE_DB}; use clarity::vm::tests::TEST_HEADER_DB; use clarity::vm::types::ResponseData; + use proptest::prelude::*; use rand::Rng; + use rstest::rstest; use stacks_common::types::chainstate::SortitionId; use stacks_common::util::hash::*; @@ -1813,6 +1861,172 @@ pub mod test { assert!(receipt.vm_error.unwrap().starts_with("DivisionByZero")); } + fn run_process_transaction_payload_at_epoch( + epoch_id: StacksEpochId, + tx: &StacksTransaction, + ) -> Result { + let marf_kv = MarfedKV::temporary(); + let chain_id = 0x80000000; + let mut clarity_instance = ClarityInstance::new(false, chain_id, marf_kv); + let mut genesis = clarity_instance.begin_test_genesis_block( + &StacksBlockId::sentinel(), + &StacksBlockHeader::make_index_block_hash( + &FIRST_BURNCHAIN_CONSENSUS_HASH, + &FIRST_STACKS_BLOCK_HASH, + ), + &TEST_HEADER_DB, + &TEST_BURN_STATE_DB, + ); + + genesis.initialize_epoch_2_05().unwrap(); + genesis.initialize_epoch_2_1().unwrap(); + genesis.initialize_epoch_3_0().unwrap(); + genesis.initialize_epoch_3_1().unwrap(); + genesis.initialize_epoch_3_2().unwrap(); + genesis.initialize_epoch_3_3().unwrap(); + if epoch_id >= StacksEpochId::Epoch34 { + genesis.initialize_epoch_3_4().unwrap(); + } + genesis.commit_block(); + + let burn_db = match epoch_id { + StacksEpochId::Epoch30 => &TestBurnStateDB_30 as &dyn BurnStateDB, + StacksEpochId::Epoch31 => &TestBurnStateDB_31 as &dyn BurnStateDB, + StacksEpochId::Epoch32 => &TestBurnStateDB_32 as &dyn BurnStateDB, + StacksEpochId::Epoch33 => &TestBurnStateDB_33 as &dyn BurnStateDB, + StacksEpochId::Epoch34 => &TestBurnStateDB_34 as &dyn BurnStateDB, + _ => panic!("Unsupported epoch in test helper: {epoch_id}"), + }; + + let next_block = clarity_instance.begin_block( + &StacksBlockHeader::make_index_block_hash( + &FIRST_BURNCHAIN_CONSENSUS_HASH, + &FIRST_STACKS_BLOCK_HASH, + ), + &StacksBlockId([3; 32]), + &TEST_HEADER_DB, + burn_db, + ); + + let mut clarity_tx = ClarityTx { + block: next_block, + config: DBConfig { + version: CHAINSTATE_VERSION.to_string(), + mainnet: false, + chain_id, + }, + }; + + let (_fee, receipt) = + validate_transactions_static_epoch_and_process_transaction(&mut clarity_tx, tx, false)?; + Ok(receipt) + } + + #[rstest] + #[case(StacksEpochId::Epoch30, false)] + #[case(StacksEpochId::Epoch31, false)] + #[case(StacksEpochId::Epoch32, false)] + #[case(StacksEpochId::Epoch33, false)] + #[case(StacksEpochId::Epoch34, true)] + fn process_transaction_payload_originator_mode_epoch_gate( + #[case] epoch_id: StacksEpochId, + #[case] should_succeed: bool, + ) { + let sk = Secp256k1PrivateKey::random(); + let auth = TransactionAuth::from_p2pkh(&sk).unwrap(); + let chain_id = 0x80000000; + + let tx = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id, + auth, + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Originator, + post_conditions: vec![], + payload: TransactionPayload::SmartContract( + TransactionSmartContract { + name: "test-contract".into(), + code_body: StacksString::from_str("(define-public (ping) (ok true))").unwrap(), + }, + None, + ), + }; + let mut signer = StacksTransactionSigner::new(&tx); + signer.sign_origin(&sk).unwrap(); + let tx = signer.get_tx().unwrap(); + + let result = run_process_transaction_payload_at_epoch(epoch_id, &tx); + if should_succeed { + let receipt = result.unwrap(); + assert_eq!(receipt.result, Value::okay_true()); + assert!(!receipt.post_condition_aborted); + } else { + match result.unwrap_err() { + Error::InvalidStacksTransaction(msg, false) => { + assert!(msg.contains("target epoch is not activated"), "{msg}"); + } + _ => panic!("Expected InvalidStacksTransaction for epoch {epoch_id:?}"), + } + }; + } + + #[rstest] + #[case(StacksEpochId::Epoch30, false)] + #[case(StacksEpochId::Epoch31, false)] + #[case(StacksEpochId::Epoch32, false)] + #[case(StacksEpochId::Epoch33, false)] + #[case(StacksEpochId::Epoch34, true)] + fn process_transaction_payload_nft_maybe_sent_epoch_gate( + #[case] epoch_id: StacksEpochId, + #[case] should_succeed: bool, + ) { + let sk = Secp256k1PrivateKey::random(); + let auth = TransactionAuth::from_p2pkh(&sk).unwrap(); + let chain_id = 0x80000000; + + let tx = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id, + auth, + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![TransactionPostCondition::Nonfungible( + PostConditionPrincipal::Origin, + AssetInfo { + contract_address: StacksAddress::new(1, Hash160([0x11; 20])).unwrap(), + contract_name: ContractName::try_from("hello-world").unwrap(), + asset_name: ClarityName::try_from("asset").unwrap(), + }, + Value::Int(1), + NonfungibleConditionCode::MaybeSent, + )], + payload: TransactionPayload::SmartContract( + TransactionSmartContract { + name: "test-contract".into(), + code_body: StacksString::from_str("(define-public (ping) (ok true))").unwrap(), + }, + None, + ), + }; + let mut signer = StacksTransactionSigner::new(&tx); + signer.sign_origin(&sk).unwrap(); + let tx = signer.get_tx().unwrap(); + + let result = run_process_transaction_payload_at_epoch(epoch_id, &tx); + if should_succeed { + let receipt = result.unwrap(); + assert_eq!(receipt.result, Value::okay_true()); + assert!(!receipt.post_condition_aborted); + } else { + match result.unwrap_err() { + Error::InvalidStacksTransaction(msg, false) => { + assert!(msg.contains("target epoch is not activated"), "{msg}"); + } + _ => panic!("Expected InvalidStacksTransaction for epoch {epoch_id:?}"), + } + }; + } + #[test] fn process_token_transfer_stx_transaction() { let mut chainstate = instantiate_chainstate(false, 0x80000000, function_name!()); @@ -7109,6 +7323,321 @@ pub mod test { } } + #[test] + fn test_check_postconditions_originator_mode_coverage() { + let privk = StacksPrivateKey::from_hex( + "6d430bb91222408e7706c9001cfaeb91b08c2be6d5ac95779ab52c6b431950e001", + ) + .unwrap(); + let auth = TransactionAuth::from_p2pkh(&privk).unwrap(); + let origin_addr = auth.origin().address_testnet(); + let origin = origin_addr.to_account_principal(); + let other_addr = StacksAddress::new(1, Hash160([0xee; 20])).unwrap(); + let other = other_addr.to_account_principal(); + + let mut mixed_stx_transfer = AssetMap::new(); + mixed_stx_transfer.add_stx_transfer(&origin, 50).unwrap(); + mixed_stx_transfer.add_stx_transfer(&other, 75).unwrap(); + + let tests = vec![ + // in originator mode, uncovered transfers from non-origin principals are permitted + ( + true, + vec![TransactionPostCondition::STX( + PostConditionPrincipal::Origin, + FungibleConditionCode::SentEq, + 50, + )], + TransactionPostConditionMode::Originator, + ), + // in originator mode, uncovered transfers from origin are forbidden + ( + false, + vec![TransactionPostCondition::STX( + PostConditionPrincipal::Standard(other_addr.clone()), + FungibleConditionCode::SentEq, + 75, + )], + TransactionPostConditionMode::Originator, + ), + // in originator mode, covering both should pass + ( + true, + vec![ + TransactionPostCondition::STX( + PostConditionPrincipal::Origin, + FungibleConditionCode::SentEq, + 50, + ), + TransactionPostCondition::STX( + PostConditionPrincipal::Standard(other_addr.clone()), + FungibleConditionCode::SentEq, + 75, + ), + ], + TransactionPostConditionMode::Originator, + ), + // sanity check: deny mode still requires all principals to be covered + ( + false, + vec![TransactionPostCondition::STX( + PostConditionPrincipal::Origin, + FungibleConditionCode::SentEq, + 50, + )], + TransactionPostConditionMode::Deny, + ), + ]; + + for (expected_result, post_conditions, mode) in tests { + let result = StacksChainState::check_transaction_postconditions( + &post_conditions, + &mode, + &make_account(&origin, 1, 123), + &mixed_stx_transfer, + Txid([0; 32]), + ) + .unwrap(); + assert_eq!( + result.is_none(), + expected_result, + "test failed:\nasset map: {mixed_stx_transfer:?}\nscenario: {post_conditions:?} mode={mode:?}" + ); + } + } + + #[test] + fn test_check_postconditions_nft_maybe_sent() { + let privk = StacksPrivateKey::from_hex( + "6d430bb91222408e7706c9001cfaeb91b08c2be6d5ac95779ab52c6b431950e001", + ) + .unwrap(); + let auth = TransactionAuth::from_p2pkh(&privk).unwrap(); + let origin_addr = auth.origin().address_testnet(); + let origin = origin_addr.to_account_principal(); + let contract_addr = StacksAddress::new(1, Hash160([0x01; 20])).unwrap(); + + let asset_info = AssetInfo { + contract_address: contract_addr.clone(), + contract_name: ContractName::try_from("hello-world").unwrap(), + asset_name: ClarityName::try_from("test-asset").unwrap(), + }; + + let asset_id = AssetIdentifier { + contract_identifier: QualifiedContractIdentifier::new( + StandardPrincipalData::from(asset_info.contract_address.clone()), + asset_info.contract_name.clone(), + ), + asset_name: asset_info.asset_name.clone(), + }; + + let mut nft_sent_value_1 = AssetMap::new(); + nft_sent_value_1.add_asset_transfer(&origin, asset_id.clone(), Value::Int(1)); + + let nft_not_sent = AssetMap::new(); + + let mut nft_sent_value_2 = AssetMap::new(); + nft_sent_value_2.add_asset_transfer(&origin, asset_id, Value::Int(2)); + + let tests = vec![ + // MAY-SEND should pass if the specified NFT is sent + ( + true, + vec![TransactionPostCondition::Nonfungible( + PostConditionPrincipal::Origin, + asset_info.clone(), + Value::Int(1), + NonfungibleConditionCode::MaybeSent, + )], + TransactionPostConditionMode::Deny, + &nft_sent_value_1, + ), + // MAY-SEND should also pass if the specified NFT is not sent + ( + true, + vec![TransactionPostCondition::Nonfungible( + PostConditionPrincipal::Origin, + asset_info.clone(), + Value::Int(1), + NonfungibleConditionCode::MaybeSent, + )], + TransactionPostConditionMode::Deny, + &nft_not_sent, + ), + // MAY-SEND covers only the specific NFT instance (value 1 does not cover value 2) + ( + false, + vec![TransactionPostCondition::Nonfungible( + PostConditionPrincipal::Origin, + asset_info.clone(), + Value::Int(1), + NonfungibleConditionCode::MaybeSent, + )], + TransactionPostConditionMode::Deny, + &nft_sent_value_2, + ), + // allow mode remains permissive regardless + ( + true, + vec![TransactionPostCondition::Nonfungible( + PostConditionPrincipal::Origin, + asset_info, + Value::Int(1), + NonfungibleConditionCode::MaybeSent, + )], + TransactionPostConditionMode::Allow, + &nft_sent_value_2, + ), + ]; + + for (expected_result, post_conditions, mode, asset_map) in tests { + let result = StacksChainState::check_transaction_postconditions( + &post_conditions, + &mode, + &make_account(&origin, 1, 123), + asset_map, + Txid([0; 32]), + ) + .unwrap(); + assert_eq!( + result.is_none(), + expected_result, + "test failed:\nasset map: {asset_map:?}\nscenario: {post_conditions:?} mode={mode:?}" + ); + } + } + + proptest! { + #[test] + fn proptest_check_postconditions_originator_mode_coverage( + origin_sent in 1u64..10_000, + other_sent in 1u64..10_000, + include_origin_check in any::(), + include_other_check in any::(), + origin_check_matches in any::(), + ) { + let privk = StacksPrivateKey::from_hex( + "6d430bb91222408e7706c9001cfaeb91b08c2be6d5ac95779ab52c6b431950e001", + ) + .unwrap(); + let auth = TransactionAuth::from_p2pkh(&privk).unwrap(); + let origin_addr = auth.origin().address_testnet(); + let origin = origin_addr.to_account_principal(); + let other_addr = StacksAddress::new(1, Hash160([0xee; 20])).unwrap(); + let other = other_addr.to_account_principal(); + + let mut asset_map = AssetMap::new(); + asset_map + .add_stx_transfer(&origin, u128::from(origin_sent)) + .unwrap(); + asset_map + .add_stx_transfer(&other, u128::from(other_sent)) + .unwrap(); + + let mut post_conditions = vec![]; + if include_origin_check { + let checked_amt = if origin_check_matches { + origin_sent + } else { + origin_sent.saturating_add(1) + }; + post_conditions.push(TransactionPostCondition::STX( + PostConditionPrincipal::Origin, + FungibleConditionCode::SentEq, + checked_amt, + )); + } + if include_other_check { + post_conditions.push(TransactionPostCondition::STX( + PostConditionPrincipal::Standard(other_addr.clone()), + FungibleConditionCode::SentEq, + other_sent, + )); + } + + let result = StacksChainState::check_transaction_postconditions( + &post_conditions, + &TransactionPostConditionMode::Originator, + &make_account(&origin, 1, 123), + &asset_map, + Txid([0; 32]), + ) + .unwrap(); + + let expected_pass = include_origin_check && origin_check_matches; + prop_assert_eq!(result.is_none(), expected_pass); + } + } + + proptest! { + #[test] + fn proptest_check_postconditions_nft_maybe_sent_variety( + checked_id in 0u16..500, + moved_id in 0u16..500, + move_asset in any::(), + mode_is_allow in any::(), + ) { + let privk = StacksPrivateKey::from_hex( + "6d430bb91222408e7706c9001cfaeb91b08c2be6d5ac95779ab52c6b431950e001", + ) + .unwrap(); + let auth = TransactionAuth::from_p2pkh(&privk).unwrap(); + let origin_addr = auth.origin().address_testnet(); + let origin = origin_addr.to_account_principal(); + + let asset_info = AssetInfo { + contract_address: StacksAddress::new(1, Hash160([0x01; 20])).unwrap(), + contract_name: ContractName::try_from("hello-world").unwrap(), + asset_name: ClarityName::try_from("test-asset").unwrap(), + }; + let asset_id = AssetIdentifier { + contract_identifier: QualifiedContractIdentifier::new( + StandardPrincipalData::from(asset_info.contract_address.clone()), + asset_info.contract_name.clone(), + ), + asset_name: asset_info.asset_name.clone(), + }; + + let mut asset_map = AssetMap::new(); + if move_asset { + asset_map.add_asset_transfer( + &origin, + asset_id, + Value::UInt(u128::from(moved_id)), + ); + } + + let mode = if mode_is_allow { + TransactionPostConditionMode::Allow + } else { + TransactionPostConditionMode::Deny + }; + + let post_conditions = vec![TransactionPostCondition::Nonfungible( + PostConditionPrincipal::Origin, + asset_info, + Value::UInt(u128::from(checked_id)), + NonfungibleConditionCode::MaybeSent, + )]; + + let result = StacksChainState::check_transaction_postconditions( + &post_conditions, + &mode, + &make_account(&origin, 1, 123), + &asset_map, + Txid([0; 32]), + ) + .unwrap(); + + let expected_pass = if mode_is_allow { + true + } else { + !move_asset || checked_id == moved_id + }; + prop_assert_eq!(result.is_none(), expected_pass); + } + } + #[test] fn test_check_postconditions_stx() { let privk = StacksPrivateKey::from_hex( @@ -8691,7 +9220,9 @@ pub mod test { .find("asks for Clarity 2, but current epoch 2.05 only supports up to Clarity 1") .is_some()); } else { - panic!("FATAL: did not recieve the appropriate error in processing a clarity2 tx in pre-2.1 epoch"); + panic!( + "FATAL: did not recieve the appropriate error in processing a clarity2 tx in pre-2.1 epoch" + ); } conn.commit_block(); diff --git a/stackslib/src/chainstate/stacks/mod.rs b/stackslib/src/chainstate/stacks/mod.rs index 0c4de218204..3732c1cea75 100644 --- a/stackslib/src/chainstate/stacks/mod.rs +++ b/stackslib/src/chainstate/stacks/mod.rs @@ -1029,6 +1029,7 @@ impl FungibleConditionCode { pub enum NonfungibleConditionCode { Sent = 0x10, NotSent = 0x11, + MaybeSent = 0x12, } impl NonfungibleConditionCode { @@ -1036,6 +1037,7 @@ impl NonfungibleConditionCode { match b { 0x10 => Some(NonfungibleConditionCode::Sent), 0x11 => Some(NonfungibleConditionCode::NotSent), + 0x12 => Some(NonfungibleConditionCode::MaybeSent), _ => None, } } @@ -1058,6 +1060,10 @@ impl NonfungibleConditionCode { NonfungibleConditionCode::NotSent => { !NonfungibleConditionCode::was_sent(nft_sent_condition, nfts_sent) } + NonfungibleConditionCode::MaybeSent => { + // always true + true + } } } } @@ -1117,8 +1123,12 @@ pub enum TransactionPostCondition { #[repr(u8)] #[derive(Debug, Clone, PartialEq, Copy, Serialize, Deserialize)] pub enum TransactionPostConditionMode { - Allow = 0x01, // allow any other changes not specified - Deny = 0x02, // deny any other changes not specified + /// allow any other changes not specified + Allow = 0x01, + /// deny any other changes not specified + Deny = 0x02, + /// deny mode for originator's assets, allow for others + Originator = 0x03, } /// Stacks transaction versions diff --git a/stackslib/src/chainstate/stacks/tests/accounting.rs b/stackslib/src/chainstate/stacks/tests/accounting.rs index b81e77ccb1e..9016affe908 100644 --- a/stackslib/src/chainstate/stacks/tests/accounting.rs +++ b/stackslib/src/chainstate/stacks/tests/accounting.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2022 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -1019,7 +1019,7 @@ fn test_get_block_info_v210() { PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| env.eval_raw(&format!("(list + |exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, &format!("(list (get-block-info? block-reward u{}) (get-block-info? miner-spend-winner u{}) (get-block-info? miner-spend-total u{}) @@ -1324,7 +1324,7 @@ fn test_get_block_info_v210_no_microblocks() { PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| env.eval_raw(&format!("(list + |exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, &format!("(list (get-block-info? block-reward u{}) (get-block-info? miner-spend-winner u{}) (get-block-info? miner-spend-total u{}) @@ -1793,7 +1793,7 @@ fn test_coinbase_pay_to_alt_recipient_v210(pay_to_contract: bool) { PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| env.eval_raw(&format!("(list + |exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, &format!("(list (get-block-info? block-reward u{}) (get-block-info? miner-spend-winner u{}) (get-block-info? miner-spend-total u{}) @@ -1823,7 +1823,7 @@ fn test_coinbase_pay_to_alt_recipient_v210(pay_to_contract: bool) { PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| env.eval_raw(&format!("(get-block-info? miner-address u{})", i)) + |exec_state, invoke_ctx| exec_state.eval_raw(invoke_ctx, &format!("(get-block-info? miner-address u{})", i)) ) .unwrap(); let miner_address = miner_val.expect_optional().unwrap().unwrap().expect_principal().unwrap(); @@ -1901,14 +1901,20 @@ fn test_coinbase_pay_to_alt_recipient_v210(pay_to_contract: bool) { PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), None, LimitedCostTracker::new_free(), - |env| { + |exec_state, invoke_ctx| { if pay_to_contract { - env.eval_raw(&format!( - "(stx-get-balance '{}.{})", - &addr_anchored, contract_name - )) + exec_state.eval_raw( + invoke_ctx, + &format!( + "(stx-get-balance '{}.{})", + &addr_anchored, contract_name + ), + ) } else { - env.eval_raw(&format!("(stx-get-balance '{})", &addr_recipient)) + exec_state.eval_raw( + invoke_ctx, + &format!("(stx-get-balance '{})", &addr_recipient), + ) } }, ) diff --git a/stackslib/src/chainstate/stacks/tests/block_construction.rs b/stackslib/src/chainstate/stacks/tests/block_construction.rs index c79c4ced35a..d2e2264082d 100644 --- a/stackslib/src/chainstate/stacks/tests/block_construction.rs +++ b/stackslib/src/chainstate/stacks/tests/block_construction.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2022 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -3752,35 +3752,44 @@ fn test_contract_call_across_clarity_versions() { PrincipalData::parse(&format!("{}", &addr_anchored)).unwrap(), Some(PrincipalData::parse(&format!("{}", &addr_anchored)).unwrap()), LimitedCostTracker::new_free(), - |env| { + |exec_state, invoke_ctx| { test_debug!("check tenure {}", tenure_id); // .contract-call? worked - let call_count_value = env - .eval_raw(&format!( - "(contract-call? '{}.test-{} get-call-count)", - &addr_anchored, tenure_id - )) + let call_count_value = exec_state + .eval_raw( + invoke_ctx, + &format!( + "(contract-call? '{}.test-{} get-call-count)", + &addr_anchored, tenure_id + ), + ) .unwrap(); let call_count = call_count_value.expect_u128().unwrap(); assert_eq!(call_count, (num_blocks - tenure_id - 1) as u128); // contract-call transaction worked - let call_count_value = env - .eval_raw(&format!( - "(contract-call? '{}.test-{} get-cc-call-count)", - &addr_anchored, tenure_id - )) + let call_count_value = exec_state + .eval_raw( + invoke_ctx, + &format!( + "(contract-call? '{}.test-{} get-cc-call-count)", + &addr_anchored, tenure_id + ), + ) .unwrap(); let call_count = call_count_value.expect_u128().unwrap(); assert_eq!(call_count, (num_blocks - tenure_id - 1) as u128); // at-block transaction worked - let at_block_count_value = env - .eval_raw(&format!( - "(contract-call? '{}.test-{} get-at-block-count)", - &addr_anchored, tenure_id - )) + let at_block_count_value = exec_state + .eval_raw( + invoke_ctx, + &format!( + "(contract-call? '{}.test-{} get-at-block-count)", + &addr_anchored, tenure_id + ), + ) .unwrap(); let call_count = at_block_count_value.expect_u128().unwrap(); diff --git a/stackslib/src/chainstate/stacks/transaction.rs b/stackslib/src/chainstate/stacks/transaction.rs index 186a71b05c8..3df47d2c3e4 100644 --- a/stackslib/src/chainstate/stacks/transaction.rs +++ b/stackslib/src/chainstate/stacks/transaction.rs @@ -649,6 +649,9 @@ impl StacksTransaction { x if x == TransactionPostConditionMode::Deny as u8 => { TransactionPostConditionMode::Deny } + x if x == TransactionPostConditionMode::Originator as u8 => { + TransactionPostConditionMode::Originator + } _ => { warn!("Invalid tx: invalid post condition mode"); return Err(codec_error::DeserializeError(format!( @@ -1838,14 +1841,12 @@ mod test { )); let mut corrupt_tx_post_condition_mode = signed_tx.clone(); - corrupt_tx_post_condition_mode.post_condition_mode = if corrupt_tx_post_condition_mode - .post_condition_mode - == TransactionPostConditionMode::Allow - { - TransactionPostConditionMode::Deny - } else { - TransactionPostConditionMode::Allow - }; + corrupt_tx_post_condition_mode.post_condition_mode = + match corrupt_tx_post_condition_mode.post_condition_mode { + TransactionPostConditionMode::Allow => TransactionPostConditionMode::Deny, + TransactionPostConditionMode::Deny => TransactionPostConditionMode::Originator, + TransactionPostConditionMode::Originator => TransactionPostConditionMode::Allow, + }; // mess with payload let mut corrupt_tx_payload = signed_tx.clone(); @@ -3608,6 +3609,116 @@ mod test { } } + #[test] + fn tx_stacks_postcondition_nft_maybe_sent_codec() { + let postcondition = TransactionPostCondition::Nonfungible( + PostConditionPrincipal::Origin, + AssetInfo { + contract_address: StacksAddress::new(1, Hash160([0x11; 20])).unwrap(), + contract_name: ContractName::try_from("contract-name").unwrap(), + asset_name: ClarityName::try_from("hello-asset").unwrap(), + }, + Value::buff_from(vec![0, 1, 2, 3]).unwrap(), + NonfungibleConditionCode::MaybeSent, + ); + + let mut postcondition_bytes = vec![]; + postcondition + .consensus_serialize(&mut postcondition_bytes) + .unwrap(); + + #[rustfmt::skip] + let expected_bytes = vec![ + // asset info id + 0x02, + // principal id (origin) + 0x01, + // contract address (version 1, Hash160([0x11; 20])) + 0x01, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + // contract name "contract-name" + 0x0d, b'c', b'o', b'n', b't', b'r', b'a', b'c', b't', b'-', b'n', b'a', b'm', b'e', + // asset name "hello-asset" + 0x0b, b'h', b'e', b'l', b'l', b'o', b'-', b'a', b's', b's', b'e', b't', + // clarity value: buffer (type prefix 0x02, length 4, data [0,1,2,3]) + 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x02, 0x03, + // condition code (MaybeSent) + 0x12, + ]; + assert_eq!(postcondition_bytes, expected_bytes); + + check_codec_and_corruption::( + &postcondition, + &postcondition_bytes, + ); + } + + #[test] + fn tx_stacks_transaction_codec_originator_mode_and_nft_maybe_sent() { + let auth = TransactionAuth::from_p2pkh(&StacksPrivateKey::random()).unwrap(); + let mut tx = StacksTransaction::new( + TransactionVersion::Testnet, + auth, + TransactionPayload::new_contract_call( + StacksAddress::new(1, Hash160([0x22; 20])).unwrap(), + "hello", + "world", + vec![Value::Int(1)], + ) + .unwrap(), + ); + + tx.post_condition_mode = TransactionPostConditionMode::Originator; + tx.post_conditions + .push(TransactionPostCondition::Nonfungible( + PostConditionPrincipal::Origin, + AssetInfo { + contract_address: StacksAddress::new(1, Hash160([0x33; 20])).unwrap(), + contract_name: ContractName::try_from("contract-name").unwrap(), + asset_name: ClarityName::try_from("hello-asset").unwrap(), + }, + Value::buff_from(vec![4, 5, 6, 7]).unwrap(), + NonfungibleConditionCode::MaybeSent, + )); + + let mut tx_bytes = vec![]; + tx.consensus_serialize(&mut tx_bytes).unwrap(); + + // Check the post-condition bytes directly within the serialized transaction + #[rustfmt::skip] + let expected_pc_bytes: &[u8] = &[ + // post-condition mode (Originator) + 0x03, + // post-conditions length prefix (1 item) + 0x00, 0x00, 0x00, 0x01, + // asset info id (NonfungibleAsset) + 0x02, + // principal id (origin) + 0x01, + // contract address (version 1, Hash160([0x33; 20])) + 0x01, + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, + // contract name "contract-name" + 0x0d, b'c', b'o', b'n', b't', b'r', b'a', b'c', b't', b'-', b'n', b'a', b'm', b'e', + // asset name "hello-asset" + 0x0b, b'h', b'e', b'l', b'l', b'o', b'-', b'a', b's', b's', b'e', b't', + // clarity value: buffer (type prefix 0x02, length 4, data [4,5,6,7]) + 0x02, 0x00, 0x00, 0x00, 0x04, 0x04, 0x05, 0x06, 0x07, + // condition code (MaybeSent) + 0x12, + ]; + assert!( + tx_bytes + .windows(expected_pc_bytes.len()) + .any(|w| w == expected_pc_bytes), + "Expected post-condition bytes not found in serialized transaction" + ); + + check_codec_and_corruption::(&tx, &tx_bytes); + } + #[test] fn tx_stacks_postcondition_invalid() { let addr = StacksAddress::new(1, Hash160([0xff; 20])).unwrap(); diff --git a/stackslib/src/chainstate/tests/parse_tests.rs b/stackslib/src/chainstate/tests/parse_tests.rs index b677503e547..697b59294a4 100644 --- a/stackslib/src/chainstate/tests/parse_tests.rs +++ b/stackslib/src/chainstate/tests/parse_tests.rs @@ -133,7 +133,7 @@ fn variant_coverage_report(variant: ParseErrorKind) { fn test_cost_balance_exceeded() { const RUNTIME_LIMIT: u64 = BLOCK_LIMIT_MAINNET_21.runtime; // Arbitrary parameters determined through empirical testing - const CONTRACT_FUNC_INVOCATIONS: u64 = 29_022; + const CONTRACT_FUNC_INVOCATIONS: u64 = 50_022; const CALL_RUNTIME_COST: u64 = 249_996_284; const CALLS_NEEDED: u64 = RUNTIME_LIMIT / CALL_RUNTIME_COST - 1; diff --git a/stackslib/src/chainstate/tests/runtime_analysis_tests.rs b/stackslib/src/chainstate/tests/runtime_analysis_tests.rs index 2d66fd875e8..fdc86399c7c 100644 --- a/stackslib/src/chainstate/tests/runtime_analysis_tests.rs +++ b/stackslib/src/chainstate/tests/runtime_analysis_tests.rs @@ -124,6 +124,7 @@ fn variant_coverage_report(variant: RuntimeCheckErrorKind) { runtime_check_error_kind_name_already_used_ccall ]), UndefinedFunction(_) => Tested(vec![runtime_check_error_kind_undefined_function_ccall]), + AtBlockUnavailable => Tested(vec![runtime_check_error_kind_at_block_unavailable_ccall]), IncorrectArgumentCount(_, _) => { Tested(vec![runtime_check_error_kind_incorrect_argument_count_ccall]) } @@ -495,6 +496,8 @@ fn runtime_check_error_kind_type_signature_too_deep_ccall() { /// that `OptionalType(NoType)` value into `is-eq` against `u0`, triggering the /// runtime `TypeError(UIntType, OptionalType(NoType))`. /// Outcome: block accepted. +/// Note: This test only works until Epoch 3.3. Epoch 3.4 will return a +/// [`RuntimeCheckErrorKind::AtBlockUnavailable`]. #[test] fn runtime_check_error_kind_type_error_cdeploy() { let contract_1 = SetupContract::new( @@ -541,6 +544,7 @@ fn runtime_check_error_kind_type_error_cdeploy() { (ok shares))) (define-constant result (get-shares u999 .pool))", + deploy_epochs: &[StacksEpochId::Epoch33], setup_contracts: &[contract_1, contract_2], ); } @@ -551,6 +555,8 @@ fn runtime_check_error_kind_type_error_cdeploy() { /// that `OptionalType(NoType)` value into `is-eq` against `u0`, triggering the /// runtime `TypeError(UIntType, OptionalType(NoType))`. /// Outcome: block accepted. +/// Note: This test only works until Epoch 3.3. Epoch 3.4 will return a +/// [`RuntimeCheckErrorKind::AtBlockUnavailable`]. #[test] fn runtime_check_error_kind_type_error_ccall() { let contract_1 = SetupContract::new( @@ -577,6 +583,9 @@ fn runtime_check_error_kind_type_error_ccall() { ) .with_clarity_version(ClarityVersion::Clarity1); // Only works with clarity 1 or 2 + let mut deploy_epochs = StacksEpochId::since(StacksEpochId::Epoch20).to_vec(); + deploy_epochs.retain(|epoch| *epoch <= StacksEpochId::Epoch33); + contract_call_consensus_test!( contract_name: "value-too-large", contract_code: " @@ -599,6 +608,8 @@ fn runtime_check_error_kind_type_error_ccall() { (get-shares u999 .pool))", function_name: "trigger-error", function_args: &[], + deploy_epochs: &deploy_epochs, + call_epochs: &[StacksEpochId::Epoch33], setup_contracts: &[contract_1, contract_2], ); } @@ -637,7 +648,7 @@ fn runtime_check_error_kind_type_value_error_ccall() { /// Outcome: block accepted. /// Note: This test only works for Clarity 2 and later. /// Clarity 1 will not be able to upload contract-3. -/// In epoch 3.4 and later, this error is not triggered because calling via a constant is allowed. +/// Even in epoch 3.4 and later, calling via a constant is not allowed at deploy time. #[test] fn runtime_check_error_kind_contract_call_expect_name_cdeploy() { let contract_1 = SetupContract::new( @@ -925,6 +936,24 @@ fn runtime_check_error_kind_undefined_function_ccall() { ); } +/// RuntimeCheckErrorKind: [`RuntimeCheckErrorKind::AtBlockUnavailable`] +/// Caused by: invoking `at-block` after crossing into Epoch 3.4, where the built-in is disabled. +/// Outcome: block accepted. +#[test] +fn runtime_check_error_kind_at_block_unavailable_ccall() { + contract_call_consensus_test!( + contract_name: "at-block-unavail", + contract_code: " + (define-public (trigger-error) + (ok (at-block 0x0101010101010101010101010101010101010101010101010101010101010101 + u1)))", + function_name: "trigger-error", + function_args: &[], + deploy_epochs: &[StacksEpochId::Epoch33], + call_epochs: &[StacksEpochId::Epoch34], + ); +} + /// RuntimeCheckErrorKind: [`RuntimeCheckErrorKind::NoSuchContract`] /// Caused by: calling a contract that does not exist. /// Outcome: block accepted. diff --git a/stackslib/src/chainstate/tests/runtime_tests.rs b/stackslib/src/chainstate/tests/runtime_tests.rs index 5d3eac7c83a..e0618a92455 100644 --- a/stackslib/src/chainstate/tests/runtime_tests.rs +++ b/stackslib/src/chainstate/tests/runtime_tests.rs @@ -664,8 +664,13 @@ fn stack_depth_too_deep_call_chain_ccall() { /// Error: [`RuntimeError::UnknownBlockHeaderHash`] /// Caused by: calling `at-block` with a block hash that doesn't exist on the current fork /// Outcome: block accepted +/// Note: This test only works until Epoch 3.3. Epoch 3.4 will return a +/// [`StaticCheckErrorKind::AtBlockUnavailable`]. #[test] fn unknown_block_header_hash_fork() { + let mut deploy_epochs = StacksEpochId::since(StacksEpochId::Epoch20).to_vec(); + deploy_epochs.retain(|epoch| *epoch <= StacksEpochId::Epoch33); + contract_call_consensus_test!( contract_name: "unknown-hash", contract_code: " @@ -679,14 +684,22 @@ fn unknown_block_header_hash_fork() { )", function_name: "trigger", function_args: &[], + deploy_epochs: &deploy_epochs, + call_epochs: &[StacksEpochId::Epoch33], ); } /// Error: [`RuntimeError::BadBlockHash`] /// Caused by: calling `at-block` with a 31-byte block hash /// Outcome: block accepted +/// Note: This test only works until Epoch 3.3. Epoch 3.4 will return a +/// [`RuntimeCheckErrorKind::AtBlockUnavailable`] during calls, and +/// [`StaticCheckErrorKind::AtBlockUnavailable`] during deployment. #[test] fn bad_block_hash() { + let mut deploy_epochs = StacksEpochId::since(StacksEpochId::Epoch20).to_vec(); + deploy_epochs.retain(|epoch| *epoch <= StacksEpochId::Epoch33); + contract_call_consensus_test!( contract_name: "bad-block-hash", contract_code: " @@ -700,6 +713,8 @@ fn bad_block_hash() { )", function_name: "trigger", function_args: &[], + deploy_epochs: &deploy_epochs, + call_epochs: &[StacksEpochId::Epoch33], ); } @@ -839,6 +854,9 @@ fn defunct_pox_contracts() { /// Error: [`RuntimeError::BlockTimeNotAvailable`] /// Caused by: attempting to retrieve the stacks-block-time from a pre-3.3 height /// Outcome: block accepted +/// Note: This test only works until Epoch 3.3. Epoch 3.4 will return a +/// [`RuntimeCheckErrorKind::AtBlockUnavailable`] during calls, and +/// [`StaticCheckErrorKind::AtBlockUnavailable`] during deployment. #[test] fn block_time_not_available() { contract_call_consensus_test!( @@ -851,7 +869,8 @@ fn block_time_not_available() { )", function_name: "trigger", function_args: &[ClarityValue::UInt(1)], - deploy_epochs: &StacksEpochId::since(StacksEpochId::Epoch33), + deploy_epochs: &[StacksEpochId::Epoch33], + call_epochs: &[StacksEpochId::Epoch33], exclude_clarity_versions: &[ClarityVersion::Clarity1, ClarityVersion::Clarity2, ClarityVersion::Clarity3], ) } diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__parse_tests__cost_balance_exceeded.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__parse_tests__cost_balance_exceeded.snap index da63079f15f..97da0b59a12 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__parse_tests__cost_balance_exceeded.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__parse_tests__cost_balance_exceeded.snap @@ -5,38 +5,38 @@ expression: result [ Failure(ExpectedFailureOutput( evaluated_epoch: Epoch33, - error: "Invalid Stacks block 3bd4519cb89a7151a1602f4e0d171b106ab80197c17a978ad0fefc79ad01fa70: CostOverflowError(ExecutionCost { write_length: 3589069, write_count: 4, read_length: 19309683, read_count: 59, runtime: 4948255138 }, ExecutionCost { write_length: 3589069, write_count: 4, read_length: 19309683, read_count: 59, runtime: 5004355279 }, ExecutionCost { write_length: 15000000, write_count: 15000, read_length: 100000000, read_count: 15000, runtime: 5000000000 })", + error: "Invalid Stacks block 814222b305da655b27ba1b21d143bd2054f2e88736de3180e4e81bd5264ccf38: CostOverflowError(ExecutionCost { write_length: 2001287, write_count: 2, read_length: 19264290, read_count: 34, runtime: 4917844782 }, ExecutionCost { write_length: 2001287, write_count: 2, read_length: 21015589, read_count: 37, runtime: 5000006934 }, ExecutionCost { write_length: 15000000, write_count: 15000, read_length: 100000000, read_count: 15000, runtime: 5000000000 })", )), Failure(ExpectedFailureOutput( evaluated_epoch: Epoch33, - error: "Invalid Stacks block e066275ee8f37230b8496164b13071d5e1015e59405451337fb01e0ecf88e92e: CostOverflowError(ExecutionCost { write_length: 3589069, write_count: 4, read_length: 19309683, read_count: 59, runtime: 4948255138 }, ExecutionCost { write_length: 3589069, write_count: 4, read_length: 19309683, read_count: 59, runtime: 5004355279 }, ExecutionCost { write_length: 15000000, write_count: 15000, read_length: 100000000, read_count: 15000, runtime: 5000000000 })", + error: "Invalid Stacks block 3409803420f77a63dcf32b61a2513d124e819652efa759ef7dfce6ddfb8bae03: CostOverflowError(ExecutionCost { write_length: 2001287, write_count: 2, read_length: 19264290, read_count: 34, runtime: 4917844782 }, ExecutionCost { write_length: 2001287, write_count: 2, read_length: 21015589, read_count: 37, runtime: 5000006934 }, ExecutionCost { write_length: 15000000, write_count: 15000, read_length: 100000000, read_count: 15000, runtime: 5000000000 })", )), Failure(ExpectedFailureOutput( evaluated_epoch: Epoch33, - error: "Invalid Stacks block 2894132b57566ba9e28fdd55b4b57c85fcd470ac6a8839d93eea957fa1d7445a: CostOverflowError(ExecutionCost { write_length: 3589069, write_count: 4, read_length: 19309683, read_count: 59, runtime: 4948255138 }, ExecutionCost { write_length: 3589069, write_count: 4, read_length: 19309683, read_count: 59, runtime: 5004355279 }, ExecutionCost { write_length: 15000000, write_count: 15000, read_length: 100000000, read_count: 15000, runtime: 5000000000 })", + error: "Invalid Stacks block 34ee57519df2dacd7e6a664001dde5d76126591527bb1f233735f3a198137767: CostOverflowError(ExecutionCost { write_length: 2001287, write_count: 2, read_length: 19264290, read_count: 34, runtime: 4917844782 }, ExecutionCost { write_length: 2001287, write_count: 2, read_length: 21015589, read_count: 37, runtime: 5000006934 }, ExecutionCost { write_length: 15000000, write_count: 15000, read_length: 100000000, read_count: 15000, runtime: 5000000000 })", )), Failure(ExpectedFailureOutput( evaluated_epoch: Epoch33, - error: "Invalid Stacks block b08f0b5cd9d192252bbd9f7804bae1b2e7582ebb6fdec867d0cb64e258a2547e: CostOverflowError(ExecutionCost { write_length: 3589069, write_count: 4, read_length: 19309683, read_count: 59, runtime: 4948255138 }, ExecutionCost { write_length: 3589069, write_count: 4, read_length: 19309683, read_count: 59, runtime: 5004355279 }, ExecutionCost { write_length: 15000000, write_count: 15000, read_length: 100000000, read_count: 15000, runtime: 5000000000 })", + error: "Invalid Stacks block 0a939efc897510dacceae7ae0983e687666e60444da7d2f5bf66e0f8b5f2df39: CostOverflowError(ExecutionCost { write_length: 2001287, write_count: 2, read_length: 19264290, read_count: 34, runtime: 4917844782 }, ExecutionCost { write_length: 2001287, write_count: 2, read_length: 21015589, read_count: 37, runtime: 5000006934 }, ExecutionCost { write_length: 15000000, write_count: 15000, read_length: 100000000, read_count: 15000, runtime: 5000000000 })", )), Failure(ExpectedFailureOutput( evaluated_epoch: Epoch34, - error: "Invalid Stacks block b7f99cf8c8b21a42e133dff840e7a3fed52053c658f7e1a14a64b645e0b7a081: CostOverflowError(ExecutionCost { write_length: 3589069, write_count: 4, read_length: 19309683, read_count: 59, runtime: 4948255138 }, ExecutionCost { write_length: 3589069, write_count: 4, read_length: 19309683, read_count: 59, runtime: 5004355279 }, ExecutionCost { write_length: 15000000, write_count: 15000, read_length: 100000000, read_count: 15000, runtime: 5000000000 })", + error: "Invalid Stacks block fe25c246f2aebd1133b4511c73c6a8db7a46d1fb1954cc7160de6da845b05cc5: CostOverflowError(ExecutionCost { write_length: 2001287, write_count: 2, read_length: 19264290, read_count: 34, runtime: 4801193478 }, ExecutionCost { write_length: 2001287, write_count: 2, read_length: 21015589, read_count: 37, runtime: 5000004459 }, ExecutionCost { write_length: 15000000, write_count: 15000, read_length: 100000000, read_count: 15000, runtime: 5000000000 })", )), Failure(ExpectedFailureOutput( evaluated_epoch: Epoch34, - error: "Invalid Stacks block 096aafac661ae1786245d6b53ec5606c13bbc185c98f9eaa5399034424a1a5e4: CostOverflowError(ExecutionCost { write_length: 3589069, write_count: 4, read_length: 19309683, read_count: 59, runtime: 4948255138 }, ExecutionCost { write_length: 3589069, write_count: 4, read_length: 19309683, read_count: 59, runtime: 5004355279 }, ExecutionCost { write_length: 15000000, write_count: 15000, read_length: 100000000, read_count: 15000, runtime: 5000000000 })", + error: "Invalid Stacks block 37c0983cb27b4c17f70f9f0d5a78936d082d50e63e64dc074535815e03ecdb74: CostOverflowError(ExecutionCost { write_length: 2001287, write_count: 2, read_length: 19264290, read_count: 34, runtime: 4801193478 }, ExecutionCost { write_length: 2001287, write_count: 2, read_length: 21015589, read_count: 37, runtime: 5000004459 }, ExecutionCost { write_length: 15000000, write_count: 15000, read_length: 100000000, read_count: 15000, runtime: 5000000000 })", )), Failure(ExpectedFailureOutput( evaluated_epoch: Epoch34, - error: "Invalid Stacks block 6a9fea7f735d2c75a43ea8149e552992b0d206bb2d186836b451d98cc62061e0: CostOverflowError(ExecutionCost { write_length: 3589069, write_count: 4, read_length: 19309683, read_count: 59, runtime: 4948255138 }, ExecutionCost { write_length: 3589069, write_count: 4, read_length: 19309683, read_count: 59, runtime: 5004355279 }, ExecutionCost { write_length: 15000000, write_count: 15000, read_length: 100000000, read_count: 15000, runtime: 5000000000 })", + error: "Invalid Stacks block 02c3d138b8d9ca243941bc459442332b15455a09158d2f52c6bf9fe0ac67b070: CostOverflowError(ExecutionCost { write_length: 2001287, write_count: 2, read_length: 19264290, read_count: 34, runtime: 4801193478 }, ExecutionCost { write_length: 2001287, write_count: 2, read_length: 21015589, read_count: 37, runtime: 5000004459 }, ExecutionCost { write_length: 15000000, write_count: 15000, read_length: 100000000, read_count: 15000, runtime: 5000000000 })", )), Failure(ExpectedFailureOutput( evaluated_epoch: Epoch34, - error: "Invalid Stacks block 37ebde6dbdba3669e5e2d253f2c96aab5ce6aecf4fab201b9ad1a80b4527fb8c: CostOverflowError(ExecutionCost { write_length: 3589069, write_count: 4, read_length: 19309683, read_count: 59, runtime: 4948255138 }, ExecutionCost { write_length: 3589069, write_count: 4, read_length: 19309683, read_count: 59, runtime: 5004355279 }, ExecutionCost { write_length: 15000000, write_count: 15000, read_length: 100000000, read_count: 15000, runtime: 5000000000 })", + error: "Invalid Stacks block 05c72569fe777aab9c4ab905e14548fe85879022f3c1c37077ffc7c61a654374: CostOverflowError(ExecutionCost { write_length: 2001287, write_count: 2, read_length: 19264290, read_count: 34, runtime: 4801193478 }, ExecutionCost { write_length: 2001287, write_count: 2, read_length: 21015589, read_count: 37, runtime: 5000004459 }, ExecutionCost { write_length: 15000000, write_count: 15000, read_length: 100000000, read_count: 15000, runtime: 5000000000 })", )), Failure(ExpectedFailureOutput( evaluated_epoch: Epoch34, - error: "Invalid Stacks block 11fcf6222d2079550ba160d33d47fd5de8546ade2cfe8647583897176b33afdd: CostOverflowError(ExecutionCost { write_length: 3589069, write_count: 4, read_length: 19309683, read_count: 59, runtime: 4948255138 }, ExecutionCost { write_length: 3589069, write_count: 4, read_length: 19309683, read_count: 59, runtime: 5004355279 }, ExecutionCost { write_length: 15000000, write_count: 15000, read_length: 100000000, read_count: 15000, runtime: 5000000000 })", + error: "Invalid Stacks block 5a4d6063367a4c802ad27d0697ba378d3e9b0699c97d6dc988c77ef639c4700a: CostOverflowError(ExecutionCost { write_length: 2001287, write_count: 2, read_length: 19264290, read_count: 34, runtime: 4801193478 }, ExecutionCost { write_length: 2001287, write_count: 2, read_length: 21015589, read_count: 37, runtime: 5000004459 }, ExecutionCost { write_length: 15000000, write_count: 15000, read_length: 100000000, read_count: 15000, runtime: 5000000000 })", )), ] diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_at_block_unavailable_ccall.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_at_block_unavailable_ccall.snap new file mode 100644 index 00000000000..30cae3ac9a5 --- /dev/null +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_at_block_unavailable_ccall.snap @@ -0,0 +1,238 @@ +--- +source: stackslib/src/chainstate/tests/runtime_analysis_tests.rs +expression: result +--- +[ + Success(ExpectedBlockOutput( + marf_hash: "595e5037bf5a3d026f48095519c8046e044e9be933239d50aad599ccf31c67a0", + evaluated_epoch: Epoch33, + transactions: [ + ExpectedTransactionOutput( + tx: "SmartContract(name: at-block-unavail-Epoch3_3-Clarity1, code_body: [..], clarity_version: Some(Clarity1))", + vm_error: "None [NON-CONSENSUS BREAKING]", + return_type: Response(ResponseData( + committed: true, + data: Bool(true), + )), + cost: ExecutionCost( + write_length: 170, + write_count: 2, + read_length: 1, + read_count: 1, + runtime: 14335, + ), + ), + ], + total_block_cost: ExecutionCost( + write_length: 170, + write_count: 2, + read_length: 1, + read_count: 1, + runtime: 14335, + ), + )), + Success(ExpectedBlockOutput( + marf_hash: "765d3d34114e4c751e1eb95b5f9a212f7ec621df95ca32373d2cc9820e483cbe", + evaluated_epoch: Epoch33, + transactions: [ + ExpectedTransactionOutput( + tx: "SmartContract(name: at-block-unavail-Epoch3_3-Clarity2, code_body: [..], clarity_version: Some(Clarity2))", + vm_error: "None [NON-CONSENSUS BREAKING]", + return_type: Response(ResponseData( + committed: true, + data: Bool(true), + )), + cost: ExecutionCost( + write_length: 170, + write_count: 2, + read_length: 1, + read_count: 1, + runtime: 14334, + ), + ), + ], + total_block_cost: ExecutionCost( + write_length: 170, + write_count: 2, + read_length: 1, + read_count: 1, + runtime: 14334, + ), + )), + Success(ExpectedBlockOutput( + marf_hash: "d80e094b09cfcbd84521f32b42e9d041a6a3480cde08e013e2e15c45f5bace5c", + evaluated_epoch: Epoch33, + transactions: [ + ExpectedTransactionOutput( + tx: "SmartContract(name: at-block-unavail-Epoch3_3-Clarity3, code_body: [..], clarity_version: Some(Clarity3))", + vm_error: "None [NON-CONSENSUS BREAKING]", + return_type: Response(ResponseData( + committed: true, + data: Bool(true), + )), + cost: ExecutionCost( + write_length: 170, + write_count: 2, + read_length: 1, + read_count: 1, + runtime: 14334, + ), + ), + ], + total_block_cost: ExecutionCost( + write_length: 170, + write_count: 2, + read_length: 1, + read_count: 1, + runtime: 14334, + ), + )), + Success(ExpectedBlockOutput( + marf_hash: "0baf0bfa59f0fc8484d807e30a75015192c67f53b209df2c436ff53a9437cbb4", + evaluated_epoch: Epoch33, + transactions: [ + ExpectedTransactionOutput( + tx: "SmartContract(name: at-block-unavail-Epoch3_3-Clarity4, code_body: [..], clarity_version: Some(Clarity4))", + vm_error: "None [NON-CONSENSUS BREAKING]", + return_type: Response(ResponseData( + committed: true, + data: Bool(true), + )), + cost: ExecutionCost( + write_length: 170, + write_count: 2, + read_length: 1, + read_count: 1, + runtime: 14334, + ), + ), + ], + total_block_cost: ExecutionCost( + write_length: 170, + write_count: 2, + read_length: 1, + read_count: 1, + runtime: 14334, + ), + )), + Success(ExpectedBlockOutput( + marf_hash: "1c49965423b15920533bc3ad20ba55b9f2d09b298789e2cd1d3d24f566a21616", + evaluated_epoch: Epoch34, + transactions: [ + ExpectedTransactionOutput( + tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: at-block-unavail-Epoch3_3-Clarity1, function_name: trigger-error, function_args: [[]])", + vm_error: "Some(AtBlockUnavailable) [NON-CONSENSUS BREAKING]", + return_type: Response(ResponseData( + committed: false, + data: Optional(OptionalData( + data: None, + )), + )), + cost: ExecutionCost( + write_length: 0, + write_count: 0, + read_length: 159, + read_count: 3, + runtime: 275, + ), + ), + ], + total_block_cost: ExecutionCost( + write_length: 0, + write_count: 0, + read_length: 159, + read_count: 3, + runtime: 275, + ), + )), + Success(ExpectedBlockOutput( + marf_hash: "66873706dda5ac24b65b37413df2a3e68ada91756fbc5c7e26326ca7f4a2cf3e", + evaluated_epoch: Epoch34, + transactions: [ + ExpectedTransactionOutput( + tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: at-block-unavail-Epoch3_3-Clarity2, function_name: trigger-error, function_args: [[]])", + vm_error: "Some(AtBlockUnavailable) [NON-CONSENSUS BREAKING]", + return_type: Response(ResponseData( + committed: false, + data: Optional(OptionalData( + data: None, + )), + )), + cost: ExecutionCost( + write_length: 0, + write_count: 0, + read_length: 159, + read_count: 3, + runtime: 275, + ), + ), + ], + total_block_cost: ExecutionCost( + write_length: 0, + write_count: 0, + read_length: 159, + read_count: 3, + runtime: 275, + ), + )), + Success(ExpectedBlockOutput( + marf_hash: "c4642b4392a4e4b822efb3ad2eae48f68a3e1e02df7f8fe0b37bda1ce469f250", + evaluated_epoch: Epoch34, + transactions: [ + ExpectedTransactionOutput( + tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: at-block-unavail-Epoch3_3-Clarity3, function_name: trigger-error, function_args: [[]])", + vm_error: "Some(AtBlockUnavailable) [NON-CONSENSUS BREAKING]", + return_type: Response(ResponseData( + committed: false, + data: Optional(OptionalData( + data: None, + )), + )), + cost: ExecutionCost( + write_length: 0, + write_count: 0, + read_length: 159, + read_count: 3, + runtime: 275, + ), + ), + ], + total_block_cost: ExecutionCost( + write_length: 0, + write_count: 0, + read_length: 159, + read_count: 3, + runtime: 275, + ), + )), + Success(ExpectedBlockOutput( + marf_hash: "eda7248b5d88d751bf9c88af97eef37f1a4729c674ebcc9d0e3ef90b1406a222", + evaluated_epoch: Epoch34, + transactions: [ + ExpectedTransactionOutput( + tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: at-block-unavail-Epoch3_3-Clarity4, function_name: trigger-error, function_args: [[]])", + vm_error: "Some(AtBlockUnavailable) [NON-CONSENSUS BREAKING]", + return_type: Response(ResponseData( + committed: false, + data: Optional(OptionalData( + data: None, + )), + )), + cost: ExecutionCost( + write_length: 0, + write_count: 0, + read_length: 159, + read_count: 3, + runtime: 275, + ), + ), + ], + total_block_cost: ExecutionCost( + write_length: 0, + write_count: 0, + read_length: 159, + read_count: 3, + runtime: 275, + ), + )), +] diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_contract_call_expect_name_ccall.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_contract_call_expect_name_ccall.snap index 4904468bb12..d100fea496e 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_contract_call_expect_name_ccall.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_contract_call_expect_name_ccall.snap @@ -295,28 +295,26 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: contract-3-Epoch3_3-Clarity2, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(ContractCallExpectName) [NON-CONSENSUS BREAKING]", + vm_error: "None [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), + committed: true, + data: Bool(true), )), cost: ExecutionCost( write_length: 0, write_count: 0, - read_length: 420, - read_count: 3, - runtime: 654, + read_length: 512, + read_count: 6, + runtime: 1045, ), ), ], total_block_cost: ExecutionCost( write_length: 0, write_count: 0, - read_length: 420, - read_count: 3, - runtime: 654, + read_length: 512, + read_count: 6, + runtime: 1045, ), )), Success(ExpectedBlockOutput( @@ -325,28 +323,26 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: contract-3-Epoch3_3-Clarity3, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(ContractCallExpectName) [NON-CONSENSUS BREAKING]", + vm_error: "None [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), + committed: true, + data: Bool(true), )), cost: ExecutionCost( write_length: 0, write_count: 0, - read_length: 420, - read_count: 3, - runtime: 654, + read_length: 512, + read_count: 6, + runtime: 1045, ), ), ], total_block_cost: ExecutionCost( write_length: 0, write_count: 0, - read_length: 420, - read_count: 3, - runtime: 654, + read_length: 512, + read_count: 6, + runtime: 1045, ), )), Success(ExpectedBlockOutput( @@ -355,28 +351,26 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: contract-3-Epoch3_3-Clarity4, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(ContractCallExpectName) [NON-CONSENSUS BREAKING]", + vm_error: "None [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), + committed: true, + data: Bool(true), )), cost: ExecutionCost( write_length: 0, write_count: 0, - read_length: 420, - read_count: 3, - runtime: 654, + read_length: 512, + read_count: 6, + runtime: 1045, ), ), ], total_block_cost: ExecutionCost( write_length: 0, write_count: 0, - read_length: 420, - read_count: 3, - runtime: 654, + read_length: 512, + read_count: 6, + runtime: 1045, ), )), Success(ExpectedBlockOutput( diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_contract_call_expect_name_cdeploy.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_contract_call_expect_name_cdeploy.snap index c89d83eafb5..902e8f411a9 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_contract_call_expect_name_cdeploy.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_contract_call_expect_name_cdeploy.snap @@ -94,115 +94,123 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "6a06cecb3093fc04925d8406d9f4c2691434bc2d4f927cf39775c445087c84d1", + marf_hash: "565d09c8a11a128ba07c4aba80557f2349d2cabe16ab9d730e2e6f5d6f30867b", evaluated_epoch: Epoch34, transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: contract-3-Epoch3_4-Clarity2, code_body: [..], clarity_version: Some(Clarity2))", - vm_error: "None [NON-CONSENSUS BREAKING]", + vm_error: "Some(ContractCallExpectName) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( - committed: true, - data: Bool(true), + committed: false, + data: Optional(OptionalData( + data: None, + )), )), cost: ExecutionCost( write_length: 273, write_count: 2, - read_length: 101, - read_count: 6, - runtime: 20295, + read_length: 9, + read_count: 3, + runtime: 19904, ), ), ], total_block_cost: ExecutionCost( write_length: 273, write_count: 2, - read_length: 101, - read_count: 6, - runtime: 20295, + read_length: 9, + read_count: 3, + runtime: 19904, ), )), Success(ExpectedBlockOutput( - marf_hash: "904e91a9d0abd2f6e79004f1e0d1602ae2ac5a1bb889a5c6865a1996b33735f2", + marf_hash: "36b6035e368d950abb490942ec4d8e031cc6dbfbea4c991ab18f920b2f1411aa", evaluated_epoch: Epoch34, transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: contract-3-Epoch3_4-Clarity3, code_body: [..], clarity_version: Some(Clarity3))", - vm_error: "None [NON-CONSENSUS BREAKING]", + vm_error: "Some(ContractCallExpectName) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( - committed: true, - data: Bool(true), + committed: false, + data: Optional(OptionalData( + data: None, + )), )), cost: ExecutionCost( write_length: 273, write_count: 2, - read_length: 101, - read_count: 6, - runtime: 20295, + read_length: 9, + read_count: 3, + runtime: 19904, ), ), ], total_block_cost: ExecutionCost( write_length: 273, write_count: 2, - read_length: 101, - read_count: 6, - runtime: 20295, + read_length: 9, + read_count: 3, + runtime: 19904, ), )), Success(ExpectedBlockOutput( - marf_hash: "310fa4144d35d35f51b5771a0cb9a237d27f1105d24382469c6c4deb87f27dd5", + marf_hash: "c31207cca304307a5537938315814873b54360ef80e83a8aefd74199b912ae69", evaluated_epoch: Epoch34, transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: contract-3-Epoch3_4-Clarity4, code_body: [..], clarity_version: Some(Clarity4))", - vm_error: "None [NON-CONSENSUS BREAKING]", + vm_error: "Some(ContractCallExpectName) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( - committed: true, - data: Bool(true), + committed: false, + data: Optional(OptionalData( + data: None, + )), )), cost: ExecutionCost( write_length: 273, write_count: 2, - read_length: 101, - read_count: 6, - runtime: 20295, + read_length: 9, + read_count: 3, + runtime: 19904, ), ), ], total_block_cost: ExecutionCost( write_length: 273, write_count: 2, - read_length: 101, - read_count: 6, - runtime: 20295, + read_length: 9, + read_count: 3, + runtime: 19904, ), )), Success(ExpectedBlockOutput( - marf_hash: "b5adb2b21ebbdd64828267ba0668a47703451970c3a93068457e3b681550828a", + marf_hash: "33b78aa4d71412fe3be808741dc69f6edfa69254501efcffc4950a9f829885f5", evaluated_epoch: Epoch34, transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: contract-3-Epoch3_4-Clarity5, code_body: [..], clarity_version: Some(Clarity5))", - vm_error: "None [NON-CONSENSUS BREAKING]", + vm_error: "Some(ContractCallExpectName) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( - committed: true, - data: Bool(true), + committed: false, + data: Optional(OptionalData( + data: None, + )), )), cost: ExecutionCost( write_length: 273, write_count: 2, - read_length: 101, - read_count: 6, - runtime: 20295, + read_length: 9, + read_count: 3, + runtime: 19904, ), ), ], total_block_cost: ExecutionCost( write_length: 273, write_count: 2, - read_length: 101, - read_count: 6, - runtime: 20295, + read_length: 9, + read_count: 3, + runtime: 19904, ), )), ] diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_could_not_determine_type_ccall.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_could_not_determine_type_ccall.snap index c9e177bd63f..34cb9ffdc83 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_could_not_determine_type_ccall.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_could_not_determine_type_ccall.snap @@ -229,7 +229,7 @@ expression: result write_count: 0, read_length: 720, read_count: 3, - runtime: 1426, + runtime: 799, ), ), ], @@ -238,7 +238,7 @@ expression: result write_count: 0, read_length: 720, read_count: 3, - runtime: 1426, + runtime: 799, ), )), ] diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_expected_contract_principal_value_ccall.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_expected_contract_principal_value_ccall.snap index de8b7e1aa9b..e5aabb78b9e 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_expected_contract_principal_value_ccall.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_expected_contract_principal_value_ccall.snap @@ -37,7 +37,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: contract-Epoch3_3-Clarity4, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(ExpectedContractPrincipalValue(Principal(Standard(StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP))))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(ExpectedContractPrincipalValue(\"Principal(Standard(StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP)))\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -123,7 +123,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: contract-Epoch3_3-Clarity4, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(ExpectedContractPrincipalValue(Principal(Standard(StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP))))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(ExpectedContractPrincipalValue(\"Principal(Standard(StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP)))\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -153,7 +153,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: contract-Epoch3_4-Clarity4, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(ExpectedContractPrincipalValue(Principal(Standard(StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP))))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(ExpectedContractPrincipalValue(\"Principal(Standard(StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP)))\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -183,7 +183,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: contract-Epoch3_4-Clarity5, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(ExpectedContractPrincipalValue(Principal(Standard(StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP))))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(ExpectedContractPrincipalValue(\"Principal(Standard(StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP)))\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_expected_contract_principal_value_cdeploy.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_expected_contract_principal_value_cdeploy.snap index d861fa60d15..d0031a12b96 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_expected_contract_principal_value_cdeploy.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_expected_contract_principal_value_cdeploy.snap @@ -9,7 +9,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: contract-Epoch3_3-Clarity4, code_body: [..], clarity_version: Some(Clarity4))", - vm_error: "Some(ExpectedContractPrincipalValue(Principal(Standard(StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP))))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(ExpectedContractPrincipalValue(\"Principal(Standard(StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP)))\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -39,7 +39,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: contract-Epoch3_4-Clarity4, code_body: [..], clarity_version: Some(Clarity4))", - vm_error: "Some(ExpectedContractPrincipalValue(Principal(Standard(StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP))))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(ExpectedContractPrincipalValue(\"Principal(Standard(StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP)))\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -69,7 +69,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: contract-Epoch3_4-Clarity5, code_body: [..], clarity_version: Some(Clarity5))", - vm_error: "Some(ExpectedContractPrincipalValue(Principal(Standard(StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP))))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(ExpectedContractPrincipalValue(\"Principal(Standard(StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP)))\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_type_error_ccall.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_type_error_ccall.snap index 1cd07a446cf..298e27a7f04 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_type_error_ccall.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_type_error_ccall.snap @@ -1453,1044 +1453,4 @@ expression: result runtime: 19452, ), )), - Success(ExpectedBlockOutput( - marf_hash: "c3e4cc52da7607c11e1bd6e4473a245915d4ccec91452c0d4017942f05e2909d", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: value-too-large-Epoch3_4-Clarity1, code_body: [..], clarity_version: Some(Clarity1))", - vm_error: "None [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 789, - write_count: 3, - read_length: 6, - read_count: 2, - runtime: 38847, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 789, - write_count: 3, - read_length: 6, - read_count: 2, - runtime: 38847, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "98f3f35a1488a8fe57cecec8410ce492a53ab4941748da690c2dbfa943bf5dde", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: value-too-large-Epoch3_4-Clarity2, code_body: [..], clarity_version: Some(Clarity2))", - vm_error: "None [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 789, - write_count: 3, - read_length: 8, - read_count: 3, - runtime: 40485, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 789, - write_count: 3, - read_length: 8, - read_count: 3, - runtime: 40485, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "2cd009999ee9048bef7165ebbc721cb7d629d93716f3bce7c1804250b8c8f103", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: value-too-large-Epoch3_4-Clarity3, code_body: [..], clarity_version: Some(Clarity3))", - vm_error: "None [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 789, - write_count: 3, - read_length: 8, - read_count: 3, - runtime: 40485, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 789, - write_count: 3, - read_length: 8, - read_count: 3, - runtime: 40485, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "9f4c864f0f72213befec7b16849bba2301093cbc6b064eca9de3d5dad09fb7e9", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: value-too-large-Epoch3_4-Clarity4, code_body: [..], clarity_version: Some(Clarity4))", - vm_error: "None [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 789, - write_count: 3, - read_length: 8, - read_count: 3, - runtime: 40485, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 789, - write_count: 3, - read_length: 8, - read_count: 3, - runtime: 40485, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "8b2176d81fbb5121fe763c70555a8ecfa929439035aaf9f62593cb78e112a9fb", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: value-too-large-Epoch3_4-Clarity5, code_body: [..], clarity_version: Some(Clarity5))", - vm_error: "None [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 789, - write_count: 3, - read_length: 8, - read_count: 3, - runtime: 40485, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 789, - write_count: 3, - read_length: 8, - read_count: 3, - runtime: 40485, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "0e003cd7822af4ad411f92d8b736236b5e4a53242ef8906af770f6ae350f380b", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch2_0-Clarity1, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "4e364c34f6fc06af32fb5b0e4eb3bc58cb02ec4cc12d4c86a536888f26c8a86c", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch2_05-Clarity1, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "3005017d884ea82979c65a89ced1ac7d2a11a0f6b50d3b8bf192fa575380af33", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch2_1-Clarity1, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "8be681baa13071e134830429a364a7130ce21a921bf1b302812fdb3fa1bae13f", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch2_1-Clarity2, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "cdf09ab6e0b98221e04371ab4397b1d0f2ba50f832c5dd22f59022d8addafc89", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch2_2-Clarity1, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "8322525e2d48f3ff0a42378e7c1b9bb9fd9b924ef7b03bbb45784d9735c93bd3", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch2_2-Clarity2, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "8c07fea37b756877b2cda36d167051de2a447d2ec4c1cda6dd04b176820d27cb", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch2_3-Clarity1, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "846016c3bf20c0b210c48d5e4f4bfeab70b9a0ca9375985cda3783fceec2a94d", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch2_3-Clarity2, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "86f783f80439b04f36ab58444b309a7d59b6a296353f71f051c40513900d7d9e", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch2_4-Clarity1, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "f68e538df336738655dd790e9e86d4c06de8fe596b387ab5fc1264ed89cc26ee", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch2_4-Clarity2, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "b288d715db986dc080fbd6b8bc67f4b105af80d7d3bc9731931cbb09382486e7", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch2_5-Clarity1, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "9df0c1c25650a9a921b50a37dcd4b0082cd423fd71a734558b9614c7cd10b62b", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch2_5-Clarity2, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "39c73f6f762abc248bea9812eefa34f30a1467bb09ac96c731a7f19f9905faa2", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch3_0-Clarity1, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "29540e609d8b45ce24f40b2a0c1e5640d92aae6e6707f63df68f2a9c7ffaa3f5", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch3_0-Clarity2, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "b82956432487d2b4d8fd2481c6297ff5b7c52fcd1bac3a84dc37dd231aa4d165", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch3_0-Clarity3, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "6f53762284838a3d9cc701d044be1dbe729a68e5d731022e437e16b79547affd", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch3_1-Clarity1, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "9f7f27b994dbcf9f8579524c815199f546d1008a05aa43ae09a636e7ef53e594", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch3_1-Clarity2, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "9135a8ade7c10090f2fddb3d713e03e593d43adc714e6247d85f78643129feff", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch3_1-Clarity3, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "3a04ccb03c792d71256631409cf41d29f7342aacf7178010d37baa39828b0bce", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch3_2-Clarity1, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "31c5508f4a8d4fa1d484fb74a72445717e698f079bf58b9b4aab94c2f1532620", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch3_2-Clarity2, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "dd3737a3c7cd2d99bef6bf24c1a9d6e9306526ec0c9722bf74f022ff046a2e7f", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch3_2-Clarity3, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "1aba310c4019082c67754e3a038b4a27d8621f902e964911798d02777e8e4529", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch3_3-Clarity1, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "eff45dabe797d7498e53c5e98a953dc0f9c6994beb33694ec78bcd63c9a8f6ce", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch3_3-Clarity2, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "adc6f83477ad2503caed1e4c654f20a249d0260f08ced1e17f23dc4397ee3778", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch3_3-Clarity3, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "a5512b2b58b4b80672d361fd080fa8213e262bdd926f155ca30e47667da3a379", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch3_3-Clarity4, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "5760c3c4aa991ba0e85c7149c57a7dd7c79b5fca34015fa879952e8cc6807b2c", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch3_4-Clarity1, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "b59a2b2c81fb63445d5a6aeff7bdf183377108e1d1859c3fd43856aff5dcdb22", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch3_4-Clarity2, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "e5dd6cc764d58981effdba7e32f9b7c28b6c03222b7ba1a3470717dd89f60c66", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch3_4-Clarity3, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "f0fe251b94aea6ce98b112417268215e72daab94dbf3bc31c0a727b8f2822b32", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch3_4-Clarity4, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "026c9c58949cec6517c8195509f306e8188bbd777e774ce7a8c00062594b1e56", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: value-too-large-Epoch3_4-Clarity5, function_name: trigger-error, function_args: [[]])", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 1095, - read_count: 10, - runtime: 19452, - ), - )), ] diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_type_error_cdeploy.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_type_error_cdeploy.snap index 9a4f91d2a9e..c5b9483d6f1 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_type_error_cdeploy.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_type_error_cdeploy.snap @@ -123,154 +123,4 @@ expression: result runtime: 59878, ), )), - Success(ExpectedBlockOutput( - marf_hash: "5ff32c7231771137de5b37d1aee118b70e98e6d09cf0fcc3ed38d639ffbb91ef", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: value-too-large-Epoch3_4-Clarity1, code_body: [..], clarity_version: Some(Clarity1))", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 811, - write_count: 3, - read_length: 470, - read_count: 9, - runtime: 58240, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 811, - write_count: 3, - read_length: 470, - read_count: 9, - runtime: 58240, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "77a0e058f3b679f83bc27c4efed64228a535fd6e65969ab255bcc577681b2604", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: value-too-large-Epoch3_4-Clarity2, code_body: [..], clarity_version: Some(Clarity2))", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 811, - write_count: 3, - read_length: 472, - read_count: 10, - runtime: 59878, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 811, - write_count: 3, - read_length: 472, - read_count: 10, - runtime: 59878, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "ba696d5fe9ee447a6d85ebd84341eabfa1dd3d7f300120823c0227a3e8a77f65", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: value-too-large-Epoch3_4-Clarity3, code_body: [..], clarity_version: Some(Clarity3))", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 811, - write_count: 3, - read_length: 472, - read_count: 10, - runtime: 59878, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 811, - write_count: 3, - read_length: 472, - read_count: 10, - runtime: 59878, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "22b185af5ba2e48269359dbe2a7fc3be05b0d10d3806a268c39cfd7358aa7079", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: value-too-large-Epoch3_4-Clarity4, code_body: [..], clarity_version: Some(Clarity4))", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 811, - write_count: 3, - read_length: 472, - read_count: 10, - runtime: 59878, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 811, - write_count: 3, - read_length: 472, - read_count: 10, - runtime: 59878, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "2040f625f346c51a41ad79b4c12e6137d99bc237aeb8189f9a6a4bba74a6177f", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: value-too-large-Epoch3_4-Clarity5, code_body: [..], clarity_version: Some(Clarity5))", - vm_error: "Some(TypeError(UIntType, OptionalType(NoType))) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 811, - write_count: 3, - read_length: 472, - read_count: 10, - runtime: 59878, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 811, - write_count: 3, - read_length: 472, - read_count: 10, - runtime: 59878, - ), - )), ] diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_type_value_error_ccall.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_type_value_error_ccall.snap index a2b20e8e090..587dba0cc3b 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_type_value_error_ccall.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_type_value_error_ccall.snap @@ -709,7 +709,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_0-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -739,7 +739,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_05-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -769,7 +769,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_1-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -799,7 +799,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_1-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -829,7 +829,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_2-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -859,7 +859,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_2-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -889,7 +889,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_3-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -919,7 +919,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_3-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -949,7 +949,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_4-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -979,7 +979,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_4-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1009,7 +1009,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_5-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1039,7 +1039,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_5-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1069,7 +1069,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_0-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1099,7 +1099,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_0-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1129,7 +1129,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_0-Clarity3, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1159,7 +1159,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_1-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1189,7 +1189,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_1-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1219,7 +1219,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_1-Clarity3, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1249,7 +1249,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_2-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1279,7 +1279,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_2-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1309,7 +1309,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_2-Clarity3, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1339,7 +1339,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_3-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1369,7 +1369,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_3-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1399,7 +1399,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_3-Clarity3, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1429,7 +1429,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_3-Clarity4, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1599,7 +1599,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_0-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1629,7 +1629,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_05-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1659,7 +1659,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_1-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1689,7 +1689,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_1-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1719,7 +1719,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_2-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1749,7 +1749,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_2-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1779,7 +1779,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_3-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1809,7 +1809,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_3-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1839,7 +1839,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_4-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1869,7 +1869,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_4-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1899,7 +1899,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_5-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1929,7 +1929,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch2_5-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1959,7 +1959,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_0-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -1989,7 +1989,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_0-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -2019,7 +2019,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_0-Clarity3, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -2049,7 +2049,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_1-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -2079,7 +2079,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_1-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -2109,7 +2109,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_1-Clarity3, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -2139,7 +2139,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_2-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -2169,7 +2169,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_2-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -2199,7 +2199,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_2-Clarity3, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -2229,7 +2229,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_3-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -2259,7 +2259,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_3-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -2289,7 +2289,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_3-Clarity3, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -2319,7 +2319,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_3-Clarity4, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -2349,7 +2349,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_4-Clarity1, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -2379,7 +2379,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_4-Clarity2, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -2409,7 +2409,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_4-Clarity3, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -2439,7 +2439,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_4-Clarity4, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -2469,7 +2469,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: check-error-kind-Epoch3_4-Clarity5, function_name: trigger-error, function_args: [[Bool(true)]])", - vm_error: "Some(TypeValueError(UIntType, Bool(true))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(UIntType, \"Bool(true)\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_type_value_error_cdeploy.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_type_value_error_cdeploy.snap index 8c094e15340..b2bd73ca5ce 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_type_value_error_cdeploy.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_type_value_error_cdeploy.snap @@ -9,7 +9,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: check-error-kind-Epoch3_3-Clarity1, code_body: [..], clarity_version: Some(Clarity1))", - vm_error: "Some(TypeValueError(SequenceType(BufferType(BufferLength(33))), Sequence(Buffer()))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(SequenceType(BufferType(BufferLength(33))), \"Sequence(Buffer())\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -39,7 +39,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: check-error-kind-Epoch3_3-Clarity2, code_body: [..], clarity_version: Some(Clarity2))", - vm_error: "Some(TypeValueError(SequenceType(BufferType(BufferLength(33))), Sequence(Buffer()))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(SequenceType(BufferType(BufferLength(33))), \"Sequence(Buffer())\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -69,7 +69,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: check-error-kind-Epoch3_3-Clarity3, code_body: [..], clarity_version: Some(Clarity3))", - vm_error: "Some(TypeValueError(SequenceType(BufferType(BufferLength(33))), Sequence(Buffer()))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(SequenceType(BufferType(BufferLength(33))), \"Sequence(Buffer())\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -99,7 +99,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: check-error-kind-Epoch3_3-Clarity4, code_body: [..], clarity_version: Some(Clarity4))", - vm_error: "Some(TypeValueError(SequenceType(BufferType(BufferLength(33))), Sequence(Buffer()))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(SequenceType(BufferType(BufferLength(33))), \"Sequence(Buffer())\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -129,7 +129,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: check-error-kind-Epoch3_4-Clarity1, code_body: [..], clarity_version: Some(Clarity1))", - vm_error: "Some(TypeValueError(SequenceType(BufferType(BufferLength(33))), Sequence(Buffer()))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(SequenceType(BufferType(BufferLength(33))), \"Sequence(Buffer())\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -159,7 +159,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: check-error-kind-Epoch3_4-Clarity2, code_body: [..], clarity_version: Some(Clarity2))", - vm_error: "Some(TypeValueError(SequenceType(BufferType(BufferLength(33))), Sequence(Buffer()))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(SequenceType(BufferType(BufferLength(33))), \"Sequence(Buffer())\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -189,7 +189,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: check-error-kind-Epoch3_4-Clarity3, code_body: [..], clarity_version: Some(Clarity3))", - vm_error: "Some(TypeValueError(SequenceType(BufferType(BufferLength(33))), Sequence(Buffer()))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(SequenceType(BufferType(BufferLength(33))), \"Sequence(Buffer())\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -219,7 +219,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: check-error-kind-Epoch3_4-Clarity4, code_body: [..], clarity_version: Some(Clarity4))", - vm_error: "Some(TypeValueError(SequenceType(BufferType(BufferLength(33))), Sequence(Buffer()))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(SequenceType(BufferType(BufferLength(33))), \"Sequence(Buffer())\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -249,7 +249,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: check-error-kind-Epoch3_4-Clarity5, code_body: [..], clarity_version: Some(Clarity5))", - vm_error: "Some(TypeValueError(SequenceType(BufferType(BufferLength(33))), Sequence(Buffer()))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(TypeValueError(SequenceType(BufferType(BufferLength(33))), \"Sequence(Buffer())\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_union_type_value_error_ccall.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_union_type_value_error_ccall.snap index 112deeaf9ae..9abe13077c9 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_union_type_value_error_ccall.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_union_type_value_error_ccall.snap @@ -37,7 +37,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: contract-2-Epoch3_3-Clarity4, function_name: trigger-runtime-error, function_args: [[]])", - vm_error: "Some(UnionTypeValueError([IntType, UIntType, BoolType, PrincipalType, SequenceType(BufferType(BufferLength(524284))), SequenceType(StringType(UTF8(StringUTF8Length(262144))))], CallableContract(CallableData { contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\"contract-1\") }, trait_identifier: Some(TraitIdentifier { name: ClarityName(\"trait-1\"), contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\"contract-2-Epoch3_3-Clarity4\") } }) }))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(UnionTypeValueError([IntType, UIntType, BoolType, PrincipalType, SequenceType(BufferType(BufferLength(524284))), SequenceType(StringType(UTF8(StringUTF8Length(262144))))], \"CallableContract(CallableData { contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\\\"contract-1\\\") }, trait_identifier: Some(TraitIdentifier { name: ClarityName(\\\"trait-1\\\"), contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\\\"contract-2-Epoch3_3-Clarity4\\\") } }) })\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -123,7 +123,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: contract-2-Epoch3_3-Clarity4, function_name: trigger-runtime-error, function_args: [[]])", - vm_error: "Some(UnionTypeValueError([IntType, UIntType, BoolType, PrincipalType, SequenceType(BufferType(BufferLength(524284))), SequenceType(StringType(UTF8(StringUTF8Length(262144))))], CallableContract(CallableData { contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\"contract-1\") }, trait_identifier: Some(TraitIdentifier { name: ClarityName(\"trait-1\"), contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\"contract-2-Epoch3_3-Clarity4\") } }) }))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(UnionTypeValueError([IntType, UIntType, BoolType, PrincipalType, SequenceType(BufferType(BufferLength(524284))), SequenceType(StringType(UTF8(StringUTF8Length(262144))))], \"CallableContract(CallableData { contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\\\"contract-1\\\") }, trait_identifier: Some(TraitIdentifier { name: ClarityName(\\\"trait-1\\\"), contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\\\"contract-2-Epoch3_3-Clarity4\\\") } }) })\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -135,7 +135,7 @@ expression: result write_count: 0, read_length: 267, read_count: 3, - runtime: 5835, + runtime: 5282, ), ), ], @@ -144,7 +144,7 @@ expression: result write_count: 0, read_length: 267, read_count: 3, - runtime: 5835, + runtime: 5282, ), )), Success(ExpectedBlockOutput( @@ -153,7 +153,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: contract-2-Epoch3_4-Clarity4, function_name: trigger-runtime-error, function_args: [[]])", - vm_error: "Some(UnionTypeValueError([IntType, UIntType, BoolType, PrincipalType, SequenceType(BufferType(BufferLength(524284))), SequenceType(StringType(UTF8(StringUTF8Length(262144))))], CallableContract(CallableData { contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\"contract-1\") }, trait_identifier: Some(TraitIdentifier { name: ClarityName(\"trait-1\"), contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\"contract-2-Epoch3_4-Clarity4\") } }) }))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(UnionTypeValueError([IntType, UIntType, BoolType, PrincipalType, SequenceType(BufferType(BufferLength(524284))), SequenceType(StringType(UTF8(StringUTF8Length(262144))))], \"CallableContract(CallableData { contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\\\"contract-1\\\") }, trait_identifier: Some(TraitIdentifier { name: ClarityName(\\\"trait-1\\\"), contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\\\"contract-2-Epoch3_4-Clarity4\\\") } }) })\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -165,7 +165,7 @@ expression: result write_count: 0, read_length: 267, read_count: 3, - runtime: 5835, + runtime: 5282, ), ), ], @@ -174,7 +174,7 @@ expression: result write_count: 0, read_length: 267, read_count: 3, - runtime: 5835, + runtime: 5282, ), )), Success(ExpectedBlockOutput( @@ -183,7 +183,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: contract-2-Epoch3_4-Clarity5, function_name: trigger-runtime-error, function_args: [[]])", - vm_error: "Some(UnionTypeValueError([IntType, UIntType, BoolType, PrincipalType, SequenceType(BufferType(BufferLength(524284))), SequenceType(StringType(UTF8(StringUTF8Length(262144))))], CallableContract(CallableData { contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\"contract-1\") }, trait_identifier: Some(TraitIdentifier { name: ClarityName(\"trait-1\"), contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\"contract-2-Epoch3_4-Clarity5\") } }) }))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(UnionTypeValueError([IntType, UIntType, BoolType, PrincipalType, SequenceType(BufferType(BufferLength(524284))), SequenceType(StringType(UTF8(StringUTF8Length(262144))))], \"CallableContract(CallableData { contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\\\"contract-1\\\") }, trait_identifier: Some(TraitIdentifier { name: ClarityName(\\\"trait-1\\\"), contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\\\"contract-2-Epoch3_4-Clarity5\\\") } }) })\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -195,7 +195,7 @@ expression: result write_count: 0, read_length: 267, read_count: 3, - runtime: 5835, + runtime: 5282, ), ), ], @@ -204,7 +204,7 @@ expression: result write_count: 0, read_length: 267, read_count: 3, - runtime: 5835, + runtime: 5282, ), )), ] diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_union_type_value_error_cdeploy.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_union_type_value_error_cdeploy.snap index cc1db325ddc..4b0e588c188 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_union_type_value_error_cdeploy.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_analysis_tests__runtime_check_error_kind_union_type_value_error_cdeploy.snap @@ -9,7 +9,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: contract-2-Epoch3_3-Clarity4, code_body: [..], clarity_version: Some(Clarity4))", - vm_error: "Some(UnionTypeValueError([IntType, UIntType, BoolType, PrincipalType, SequenceType(BufferType(BufferLength(524284))), SequenceType(StringType(UTF8(StringUTF8Length(262144))))], CallableContract(CallableData { contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\"contract-1\") }, trait_identifier: Some(TraitIdentifier { name: ClarityName(\"trait-1\"), contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\"contract-2-Epoch3_3-Clarity4\") } }) }))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(UnionTypeValueError([IntType, UIntType, BoolType, PrincipalType, SequenceType(BufferType(BufferLength(524284))), SequenceType(StringType(UTF8(StringUTF8Length(262144))))], \"CallableContract(CallableData { contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\\\"contract-1\\\") }, trait_identifier: Some(TraitIdentifier { name: ClarityName(\\\"trait-1\\\"), contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\\\"contract-2-Epoch3_3-Clarity4\\\") } }) })\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -39,7 +39,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: contract-2-Epoch3_4-Clarity4, code_body: [..], clarity_version: Some(Clarity4))", - vm_error: "Some(UnionTypeValueError([IntType, UIntType, BoolType, PrincipalType, SequenceType(BufferType(BufferLength(524284))), SequenceType(StringType(UTF8(StringUTF8Length(262144))))], CallableContract(CallableData { contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\"contract-1\") }, trait_identifier: Some(TraitIdentifier { name: ClarityName(\"trait-1\"), contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\"contract-2-Epoch3_4-Clarity4\") } }) }))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(UnionTypeValueError([IntType, UIntType, BoolType, PrincipalType, SequenceType(BufferType(BufferLength(524284))), SequenceType(StringType(UTF8(StringUTF8Length(262144))))], \"CallableContract(CallableData { contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\\\"contract-1\\\") }, trait_identifier: Some(TraitIdentifier { name: ClarityName(\\\"trait-1\\\"), contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\\\"contract-2-Epoch3_4-Clarity4\\\") } }) })\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -51,7 +51,7 @@ expression: result write_count: 2, read_length: 3, read_count: 2, - runtime: 25164, + runtime: 24611, ), ), ], @@ -60,7 +60,7 @@ expression: result write_count: 2, read_length: 3, read_count: 2, - runtime: 25164, + runtime: 24611, ), )), Success(ExpectedBlockOutput( @@ -69,7 +69,7 @@ expression: result transactions: [ ExpectedTransactionOutput( tx: "SmartContract(name: contract-2-Epoch3_4-Clarity5, code_body: [..], clarity_version: Some(Clarity5))", - vm_error: "Some(UnionTypeValueError([IntType, UIntType, BoolType, PrincipalType, SequenceType(BufferType(BufferLength(524284))), SequenceType(StringType(UTF8(StringUTF8Length(262144))))], CallableContract(CallableData { contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\"contract-1\") }, trait_identifier: Some(TraitIdentifier { name: ClarityName(\"trait-1\"), contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\"contract-2-Epoch3_4-Clarity5\") } }) }))) [NON-CONSENSUS BREAKING]", + vm_error: "Some(UnionTypeValueError([IntType, UIntType, BoolType, PrincipalType, SequenceType(BufferType(BufferLength(524284))), SequenceType(StringType(UTF8(StringUTF8Length(262144))))], \"CallableContract(CallableData { contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\\\"contract-1\\\") }, trait_identifier: Some(TraitIdentifier { name: ClarityName(\\\"trait-1\\\"), contract_identifier: QualifiedContractIdentifier { issuer: StandardPrincipalData(ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP), name: ContractName(\\\"contract-2-Epoch3_4-Clarity5\\\") } }) })\")) [NON-CONSENSUS BREAKING]", return_type: Response(ResponseData( committed: false, data: Optional(OptionalData( @@ -81,7 +81,7 @@ expression: result write_count: 2, read_length: 3, read_count: 2, - runtime: 25164, + runtime: 24611, ), ), ], @@ -90,7 +90,7 @@ expression: result write_count: 2, read_length: 3, read_count: 2, - runtime: 25164, + runtime: 24611, ), )), ] diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_tests__bad_block_hash.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_tests__bad_block_hash.snap index e8123e10428..052f69dfe23 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_tests__bad_block_hash.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_tests__bad_block_hash.snap @@ -1453,1044 +1453,4 @@ expression: result runtime: 1584, ), )), - Success(ExpectedBlockOutput( - marf_hash: "bde5cc20fa5d0d499db1f15d25455552774171f2811571fcfc2e36ab998957c7", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: bad-block-hash-Epoch3_4-Clarity1, code_body: [..], clarity_version: Some(Clarity1))", - vm_error: "None [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 155, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 13907, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 155, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 13907, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "7d4f180785e4fbc68831c679cd00f07bb5e6a1e9ee1ba8062816d82e72e3140b", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: bad-block-hash-Epoch3_4-Clarity2, code_body: [..], clarity_version: Some(Clarity2))", - vm_error: "None [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 155, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 13906, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 155, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 13906, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "9d909a3a10287f8c49205a75d54da29168caa8f7446debe20c6d36e54a0dcc5c", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: bad-block-hash-Epoch3_4-Clarity3, code_body: [..], clarity_version: Some(Clarity3))", - vm_error: "None [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 155, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 13906, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 155, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 13906, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "5f8c569a99f7d3cbef326ed8317ecedbe4bf78fbf171da26728dab0554638d6c", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: bad-block-hash-Epoch3_4-Clarity4, code_body: [..], clarity_version: Some(Clarity4))", - vm_error: "None [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 155, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 13906, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 155, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 13906, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "b6e461c2ee5fe6473e253a2590a0e7fa620c5e735d7f2abdcf34dd1419bd1ae4", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: bad-block-hash-Epoch3_4-Clarity5, code_body: [..], clarity_version: Some(Clarity5))", - vm_error: "None [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 155, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 13906, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 155, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 13906, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "b2475afa2816fc78d0e1f32063c3287ccfff6aecea765208e07f9b02d70aa3c9", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch2_0-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "8129a864f4e82f7d3991ef6754254a9588bad9d21f5e0b9f1cbea48cfc5a5bb2", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch2_05-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "1c777fe8a341a877512b0ae7fe895e270cb57e7b874b1a6b0b69a230ad77eeee", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch2_1-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "2a7c952fdb6e7f51944969a295dc7941a699cd00b018466873db89cbffd1527e", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch2_1-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "2516352feff5554225828e587c6bf9de6d29d029d73e37f045f4a67ea20f4e0d", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch2_2-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "1b7dca1e57e74318144e32d75f17526edb7395164f077bfe23adeb96b044fbf3", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch2_2-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "171d4be8af6a5bae59286b48ffc43d1fb7236708288f6a7f4599c16b97ba45d0", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch2_3-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "fbff7cccc979fc66e7d7d7c945ce8c751223b385adec83e337f91ffacac154c5", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch2_3-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "1e1df872a8a705697890938394b7cb0938feb0ae29f10311d200f3d93f41481b", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch2_4-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "1275eb6e4b74a340df9cbb816cc7fc1d48ace24eb575f5bf6b210a996142a8bf", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch2_4-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "04c88a9406def0654ac8e724694322951f5e40efd3a0216ef5f37f076768f262", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch2_5-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "f619b5a1a765dd43c25e9191588a891a9ddfc1f6f6a3c6bd6b0ebbc40a9d8a0a", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch2_5-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "e29f8997ef0928adee5443fa022f5752f2c99ba22c643753ca4d9c36239a2ab3", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch3_0-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "aa4b8d4d88620b529672f6048631f977f8b215954a8e1ae92b106a6ba9dcdb64", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch3_0-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "0931abd1b9e3fdda85f023ba909e70cac298fe4accd9ae47984a9abd53cd8e3a", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch3_0-Clarity3, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "f6fc8fef9f290cef344b995066126a8c901d17b0c0c02a354048708d273cab6c", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch3_1-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "c275bb5d1dbe22911417166d9a637905f8dffe0dcef0e710959d6c6cdf467e2e", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch3_1-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "69f6d74ae1b74e7176faa267d0bad07bc86e695d42a2d021058cdec384ab2189", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch3_1-Clarity3, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "a66bb607030f8fc81c714c40a03b17bb646ce69966cb86e536d7352e2be715fb", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch3_2-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "269ee5f71eebecaf79d05ff0229f55fc371ab3b539169a06bada3fdbecce5d3e", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch3_2-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "c0a90a1077eff63fb7e0353c68ff019a56fb2fbac6b5135a17fb0588fe5b5a9a", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch3_2-Clarity3, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "3f3cb1ca6a0aec5c2606183746c56b531671a8fa813894b80240b9ad58a823ee", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch3_3-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "5d020310561f383d4e484d72e378ce916bf8ca102f24df299677b0fe60dae0d7", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch3_3-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "0ea154210aeeeb242c882aef0541ef954e4c9c6fc7aea1cff0e088be19dfa5ed", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch3_3-Clarity3, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "78b4fa9692ce4f911cb2adb4ca0f4051647e69a73e60b3f5e0877df701a02f0b", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch3_3-Clarity4, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "0eaf82c18cd482a0e2287ad4ac4a815008c62a387fbf92e84b0a8f515b114b46", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch3_4-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "854b8c4386506e4176493cd1e97ec322d336f7abf49e68074b1221b97f2bb4ae", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch3_4-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "a3503f7c5bf5dd3dd5f00d6ae6b0655292340d1f84cada1c42dc1f40d91897dc", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch3_4-Clarity3, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "ba08191e36aaea9f7e3a17c4442db127a0ee870a6a217538215cd721e92377b4", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch3_4-Clarity4, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "99f0e0c2c204f8c839b5d71c73447d28888a48ea8d167f8e5cbecd1e3a2192b1", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: bad-block-hash-Epoch3_4-Clarity5, function_name: trigger, function_args: [[]])", - vm_error: "Some(BadBlockHash([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30])) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 142, - read_count: 4, - runtime: 1584, - ), - )), ] diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_tests__block_time_not_available.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_tests__block_time_not_available.snap index 87e8836be5c..5f5dc5ad20e 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_tests__block_time_not_available.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_tests__block_time_not_available.snap @@ -61,150 +61,4 @@ expression: result runtime: 9048, ), )), - Success(ExpectedBlockOutput( - marf_hash: "8c52c971290c0293b81101f373b5cf9a27beb9bc9201219e2956960bce955696", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: no-block-time-Epoch3_4-Clarity4, code_body: [..], clarity_version: Some(Clarity4))", - vm_error: "None [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 219, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 16360, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 219, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 16360, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "94175893b031d4b0aa981ee0e6d0db8262e0adea28ed57360a15d2b6b8acf412", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: no-block-time-Epoch3_4-Clarity5, code_body: [..], clarity_version: Some(Clarity5))", - vm_error: "None [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 219, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 16360, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 219, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 16360, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "af41c1944e0fc289d6551d58c9311ffb8d91afc884a9dad1332fd737f21f6aca", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: no-block-time-Epoch3_3-Clarity4, function_name: trigger, function_args: [[UInt(1)]])", - vm_error: "Some(BlockTimeNotAvailable) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 201, - read_count: 6, - runtime: 9048, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 201, - read_count: 6, - runtime: 9048, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "3311002de6350fc92f5283a741f11609d2aeedbddc977e241cc56603b212820c", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: no-block-time-Epoch3_4-Clarity4, function_name: trigger, function_args: [[UInt(1)]])", - vm_error: "Some(BlockTimeNotAvailable) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 201, - read_count: 6, - runtime: 9048, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 201, - read_count: 6, - runtime: 9048, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "52d35d9a448a8b2e926de13759f06b22275f4a8d2d906a5eb8249e05e71f87d5", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: no-block-time-Epoch3_4-Clarity5, function_name: trigger, function_args: [[UInt(1)]])", - vm_error: "Some(BlockTimeNotAvailable) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 201, - read_count: 6, - runtime: 9048, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 201, - read_count: 6, - runtime: 9048, - ), - )), ] diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_tests__defunct_pox_contracts.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_tests__defunct_pox_contracts.snap index b1f815d4d46..c769a4e7641 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_tests__defunct_pox_contracts.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_tests__defunct_pox_contracts.snap @@ -21,7 +21,7 @@ expression: results write_count: 4, read_length: 31964, read_count: 21, - runtime: 367029, + runtime: 366006, ), ), ], @@ -30,7 +30,7 @@ expression: results write_count: 4, read_length: 31964, read_count: 21, - runtime: 367029, + runtime: 366006, ), )), Success(ExpectedBlockOutput( @@ -51,7 +51,7 @@ expression: results write_count: 4, read_length: 67749, read_count: 21, - runtime: 592784, + runtime: 590903, ), ), ], @@ -60,7 +60,7 @@ expression: results write_count: 4, read_length: 67749, read_count: 21, - runtime: 592784, + runtime: 590903, ), )), Success(ExpectedBlockOutput( @@ -81,7 +81,7 @@ expression: results write_count: 4, read_length: 68458, read_count: 21, - runtime: 593569, + runtime: 591688, ), ), ], @@ -90,7 +90,7 @@ expression: results write_count: 4, read_length: 68458, read_count: 21, - runtime: 593569, + runtime: 591688, ), )), Success(ExpectedBlockOutput( @@ -160,7 +160,7 @@ expression: results write_count: 6, read_length: 77538, read_count: 23, - runtime: 710119, + runtime: 708033, ), ), ], @@ -169,7 +169,7 @@ expression: results write_count: 6, read_length: 77538, read_count: 23, - runtime: 710119, + runtime: 708033, ), )), ] diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_tests__unknown_block_header_hash_fork.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_tests__unknown_block_header_hash_fork.snap index 7bda62a39ea..7721ba88258 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_tests__unknown_block_header_hash_fork.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__runtime_tests__unknown_block_header_hash_fork.snap @@ -1453,1044 +1453,4 @@ expression: result runtime: 1588, ), )), - Success(ExpectedBlockOutput( - marf_hash: "a21046cf929bd3cdbae8afee8dc800334eb5d6a17db1e8e2a2bcc6e47137cbd2", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: unknown-hash-Epoch3_4-Clarity1, code_body: [..], clarity_version: Some(Clarity1))", - vm_error: "None [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 159, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 14059, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 159, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 14059, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "9b0c8a6b2d88069aa380d517193e56ec2a2c71e9a191219439d047e3a0e98ca1", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: unknown-hash-Epoch3_4-Clarity2, code_body: [..], clarity_version: Some(Clarity2))", - vm_error: "None [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 159, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 14058, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 159, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 14058, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "ce9d6120ffa2bdc33858ccc22c81a96627b0549a53c7b2def36f75ec6d6f0069", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: unknown-hash-Epoch3_4-Clarity3, code_body: [..], clarity_version: Some(Clarity3))", - vm_error: "None [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 159, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 14058, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 159, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 14058, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "40c7e82acdaf48d2a83b6fa2872f928b018ce3dd91681f3cb59c796af5e99d8e", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: unknown-hash-Epoch3_4-Clarity4, code_body: [..], clarity_version: Some(Clarity4))", - vm_error: "None [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 159, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 14058, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 159, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 14058, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "d5df4cf235ec97bdda08c3579a64bfbfcf1050f78028570cff1e038bb775c8cb", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: unknown-hash-Epoch3_4-Clarity5, code_body: [..], clarity_version: Some(Clarity5))", - vm_error: "None [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 159, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 14058, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 159, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 14058, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "a579418ee51194fb9db06c7624f1cc44dd68f0d0c2315d17eb4dbb7d1db575f6", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch2_0-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "176d850259dfc95056c9609288773b0783d297ef4ddfa01a3990f9e5e3728c65", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch2_05-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "0d535403f79e7db2e4c58dea9990d1a1545e18c279fa8f0a31a2075789d29528", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch2_1-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "646e3e72d597d2e40f33f7ec53cb9fdcc6dbc1cd31a12cf1ecf81aa204ae3821", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch2_1-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "d40550d187401d4f085a579fbe96533ea95448393f8bce5150977e1e5c2f4775", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch2_2-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "5d4e7de39799feb5d2cf52ca2ed8f25d38f5a050cb34e4648e0d40e2a633dde3", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch2_2-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "b177b0652334364f7a458ee27b906afe5e20f4bcb8e76ff15af68ddf5cf58624", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch2_3-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "61f496314c3d382bbc033b7501f8f2d53d5df70bcd4f9142cc165be57f373143", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch2_3-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "a8019336aae3d953244690c07f1303d90bf604137e3e0adfa8054ed77f8fe0a7", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch2_4-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "44ee49be5d6b66b17abae6399ac43c1119e3a5f61bc0a70815b4632a79332f6b", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch2_4-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "453e02a024fb941ad4034237d497c97375b8cc008e8daab3c597283a048e1882", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch2_5-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "f042eb1b94088f630a379ec70ec183b6b2ed5037776173241c1ded12d498bb51", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch2_5-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "b143b6026de63806e32239f4e27b9b59101f528e7808560809cc7527559f628a", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch3_0-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "33c4d7cc26a02caaf676bbc151b573e0a16f1611ee0471c026da33f95bd94e62", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch3_0-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "7f4c6bc463b0e82fe398622904164d74a382c40f3bd0c536f3c126afd2270c18", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch3_0-Clarity3, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "6200c5062717262a118fb64b35b700d98289067c19bddddbddc7793012ca63d7", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch3_1-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "ab12752a719958a9b9135bcddd362bbdb0556181af889e32bc623c13401b8760", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch3_1-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "7dfaa4004347c826475a404c834792650f8959248407373e77c49d28b7ace9fa", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch3_1-Clarity3, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "b071c1c4210fe0cb082dec3b6d54969260f33b68a0ec64840ea1f590a70dbc11", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch3_2-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "b9ba1325a0bc8db781871ed86b11009b0de04832a913557c70e9a2593fcbde4f", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch3_2-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "a2b80fbcc0a699e3a65ad5891732fb94c738689da95aa365d324d3ca43769654", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch3_2-Clarity3, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "75d67aeacbea51d57033fe18f25665f194f8c838aa22e57d4d63c5ffa0974fdb", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch3_3-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "cf8fea7c12783b50cb0f7955f75b29a704669238ec8cf72bf5f577a36dbb2cf6", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch3_3-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "1edcec8b9b96bfa883175e5f00c0c26aeaecdce276ccdf12574a8dc631b0c7aa", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch3_3-Clarity3, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "e0b536959ee6a066018b5521ba645e1c0057633dc5990b36d3d9281f33944da1", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch3_3-Clarity4, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "452eca020283808d51210e376e76fcb77e2420781b0cef4c0185ccbf21eb5cae", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch3_4-Clarity1, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "32c2d0b30cb7d737d7693f5e86f99ccb20b0ef1f324476c8099e783ef7c37027", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch3_4-Clarity2, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "aec3b07409bf3234fdd9eac688ad13e4d507548b95b036ae926d75bafc85342f", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch3_4-Clarity3, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "eb1f70ccd206fec71917b1b50b1a3616e8a280369cf1e83a3fd473f9779f38c1", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch3_4-Clarity4, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), - Success(ExpectedBlockOutput( - marf_hash: "f953dc27ffb1dc5c963e165fdd37f791d7441543e82d7b8dfdb7acdc958ba827", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "ContractCall(address: ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP, contract_name: unknown-hash-Epoch3_4-Clarity5, function_name: trigger, function_args: [[]])", - vm_error: "Some(UnknownBlockHeaderHash(0202020202020202020202020202020202020202020202020202020202020202)) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 146, - read_count: 4, - runtime: 1588, - ), - )), ] diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__static_analysis_tests__static_check_error_at_block_closure_must_be_read_only.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__static_analysis_tests__static_check_error_at_block_closure_must_be_read_only.snap index 24a67d9b2ab..af1abc95ec5 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__static_analysis_tests__static_check_error_at_block_closure_must_be_read_only.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__static_analysis_tests__static_check_error_at_block_closure_must_be_read_only.snap @@ -243,34 +243,4 @@ expression: result runtime: 3966, ), )), - Success(ExpectedBlockOutput( - marf_hash: "8ad444a23ba5024ba933d2c3f2e539d940b14f0294cc1245ebb9a8856557dace", - evaluated_epoch: Epoch34, - transactions: [ - ExpectedTransactionOutput( - tx: "SmartContract(name: closure-must-be-ro-Epoch3_4-Clarity5, code_body: [..], clarity_version: Some(Clarity5))", - vm_error: "Some(:0:0: (at-block ...) closures expect read-only statements, but detected a writing operation) [NON-CONSENSUS BREAKING]", - return_type: Response(ResponseData( - committed: false, - data: Optional(OptionalData( - data: None, - )), - )), - cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 0, - read_count: 0, - runtime: 3966, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 0, - read_count: 0, - runtime: 3966, - ), - )), ] diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__static_analysis_tests__static_check_error_at_block_unavailable.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__static_analysis_tests__static_check_error_at_block_unavailable.snap new file mode 100644 index 00000000000..b1799422c04 --- /dev/null +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__static_analysis_tests__static_check_error_at_block_unavailable.snap @@ -0,0 +1,126 @@ +--- +source: stackslib/src/chainstate/tests/static_analysis_tests.rs +expression: result +--- +[ + Success(ExpectedBlockOutput( + marf_hash: "0e6318cf2be7117335040e469d68ea0fcacbd2ed2bd8282cfb7252b913a9bc5a", + evaluated_epoch: Epoch34, + transactions: [ + ExpectedTransactionOutput( + tx: "SmartContract(name: at-block-unavailable-Epoch3_4-Clarity1, code_body: [..], clarity_version: Some(Clarity1))", + vm_error: "Some(:0:0: (at-block ...) is not available in this epoch) [NON-CONSENSUS BREAKING]", + return_type: Response(ResponseData( + committed: false, + data: Optional(OptionalData( + data: None, + )), + )), + cost: ExecutionCost( + write_length: 11, + write_count: 1, + read_length: 1, + read_count: 1, + runtime: 4540, + ), + ), + ], + total_block_cost: ExecutionCost( + write_length: 11, + write_count: 1, + read_length: 1, + read_count: 1, + runtime: 4540, + ), + )), + Success(ExpectedBlockOutput( + marf_hash: "70ce55de58359b3350d9eff19cc30d592145b2373046d8bdddb73d86227e3911", + evaluated_epoch: Epoch34, + transactions: [ + ExpectedTransactionOutput( + tx: "SmartContract(name: at-block-unavailable-Epoch3_4-Clarity2, code_body: [..], clarity_version: Some(Clarity2))", + vm_error: "Some(:0:0: (at-block ...) is not available in this epoch) [NON-CONSENSUS BREAKING]", + return_type: Response(ResponseData( + committed: false, + data: Optional(OptionalData( + data: None, + )), + )), + cost: ExecutionCost( + write_length: 11, + write_count: 1, + read_length: 1, + read_count: 1, + runtime: 4540, + ), + ), + ], + total_block_cost: ExecutionCost( + write_length: 11, + write_count: 1, + read_length: 1, + read_count: 1, + runtime: 4540, + ), + )), + Success(ExpectedBlockOutput( + marf_hash: "ef0d91ef8a5574b7d7c9988cd67dab3352daad6953867ab6e13298772035c5a8", + evaluated_epoch: Epoch34, + transactions: [ + ExpectedTransactionOutput( + tx: "SmartContract(name: at-block-unavailable-Epoch3_4-Clarity3, code_body: [..], clarity_version: Some(Clarity3))", + vm_error: "Some(:0:0: (at-block ...) is not available in this epoch) [NON-CONSENSUS BREAKING]", + return_type: Response(ResponseData( + committed: false, + data: Optional(OptionalData( + data: None, + )), + )), + cost: ExecutionCost( + write_length: 11, + write_count: 1, + read_length: 1, + read_count: 1, + runtime: 4540, + ), + ), + ], + total_block_cost: ExecutionCost( + write_length: 11, + write_count: 1, + read_length: 1, + read_count: 1, + runtime: 4540, + ), + )), + Success(ExpectedBlockOutput( + marf_hash: "549475b1df5a68d5d3f6a0fd3aa83d58dc3b9d469fa191dc504752e4c60f3af6", + evaluated_epoch: Epoch34, + transactions: [ + ExpectedTransactionOutput( + tx: "SmartContract(name: at-block-unavailable-Epoch3_4-Clarity4, code_body: [..], clarity_version: Some(Clarity4))", + vm_error: "Some(:0:0: (at-block ...) is not available in this epoch) [NON-CONSENSUS BREAKING]", + return_type: Response(ResponseData( + committed: false, + data: Optional(OptionalData( + data: None, + )), + )), + cost: ExecutionCost( + write_length: 11, + write_count: 1, + read_length: 1, + read_count: 1, + runtime: 4540, + ), + ), + ], + total_block_cost: ExecutionCost( + write_length: 11, + write_count: 1, + read_length: 1, + read_count: 1, + runtime: 4540, + ), + )), +] diff --git a/stackslib/src/chainstate/tests/static_analysis_tests.rs b/stackslib/src/chainstate/tests/static_analysis_tests.rs index 666afd310ba..695d59415e0 100644 --- a/stackslib/src/chainstate/tests/static_analysis_tests.rs +++ b/stackslib/src/chainstate/tests/static_analysis_tests.rs @@ -168,6 +168,7 @@ fn variant_coverage_report(variant: StaticCheckErrorKind) { TraitTooManyMethods(_, _) => Tested(vec![static_check_error_trait_too_many_methods]), WriteAttemptedInReadOnly => Tested(vec![static_check_error_write_attempted_in_read_only]), AtBlockClosureMustBeReadOnly => Tested(vec![static_check_error_at_block_closure_must_be_read_only]), + AtBlockUnavailable => Tested(vec![static_check_error_at_block_unavailable]), ExpectedListOfAllowances(_, _) => Tested(vec![static_check_error_expected_list_of_allowances]), AllowanceExprNotAllowed => Tested(vec![static_check_error_allowance_expr_not_allowed]), ExpectedAllowanceExpr(_) => Tested(vec![static_check_error_expected_allowance_expr]), @@ -1217,8 +1218,13 @@ fn static_check_error_write_attempted_in_read_only() { /// StaticCheckErrorKind: [`StaticCheckErrorKind::AtBlockClosureMustBeReadOnly`] /// Caused by: `at-block` closure must be read-only but contains write operations. /// Outcome: block accepted. +/// Note: In Clarity5+, `at-block` is removed from the language, so this same +/// contract fails earlier with `UnknownFunction("at-block")`. #[test] fn static_check_error_at_block_closure_must_be_read_only() { + let mut exclude_clarity_versions = ClarityVersion::ALL.to_vec(); + exclude_clarity_versions.retain(|version| *version > ClarityVersion::Clarity4); + contract_deploy_consensus_test!( contract_name: "closure-must-be-ro", contract_code: " @@ -1226,6 +1232,27 @@ fn static_check_error_at_block_closure_must_be_read_only() { (define-private (foo-bar) (at-block (sha256 0) (var-set foo 0)))", + exclude_clarity_versions: &exclude_clarity_versions, + ); +} + +/// StaticCheckErrorKind: [`StaticCheckErrorKind::AtBlockUnavailable`] +/// Caused by: using `at-block` in Epoch 3.4+, where the built-in is disabled. +/// Outcome: block accepted. +/// Note: In Clarity5+, `at-block` is removed from the language surface, so this same +/// contract fails earlier with `UnknownFunction("at-block")`. +#[test] +fn static_check_error_at_block_unavailable() { + let mut exclude_clarity_versions = ClarityVersion::ALL.to_vec(); + exclude_clarity_versions.retain(|version| *version > ClarityVersion::Clarity4); + contract_deploy_consensus_test!( + contract_name: "at-block-unavailable", + contract_code: " + (define-public (trigger-error) + (ok (at-block 0x0101010101010101010101010101010101010101010101010101010101010101 + u1)))", + deploy_epochs: &StacksEpochId::since(StacksEpochId::Epoch34), + exclude_clarity_versions: &exclude_clarity_versions, ); } diff --git a/stackslib/src/clarity_vm/clarity.rs b/stackslib/src/clarity_vm/clarity.rs index f4a58044e59..891a299bc1a 100644 --- a/stackslib/src/clarity_vm/clarity.rs +++ b/stackslib/src/clarity_vm/clarity.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -1943,7 +1943,7 @@ impl<'a, 'b> ClarityBlockConnection<'a, 'b> { tx_conn.epoch = StacksEpochId::Epoch34; }); - debug!("Epoch 3.4 initialized"); + info!("Epoch 3.4 initialized"); (old_cost_tracker, Ok(vec![])) }) } @@ -2198,10 +2198,11 @@ impl ClarityTransactionConnection<'_, '_> { self.with_abort_callback( |vm_env| { vm_env - .execute_in_env(sender.clone(), None, None, |env| { - env.run_as_transaction(|env| { + .execute_in_env(sender.clone(), None, None, |exec_state, invoke_ctx| { + exec_state.run_as_transaction(invoke_ctx, |exec_state, invoke_ctx| { StacksChainState::handle_poison_microblock( - env, + exec_state, + invoke_ctx, mblock_header_1, mblock_header_2, ) diff --git a/stackslib/src/clarity_vm/tests/events.rs b/stackslib/src/clarity_vm/tests/events.rs index 20f2678306e..9a385b99894 100644 --- a/stackslib/src/clarity_vm/tests/events.rs +++ b/stackslib/src/clarity_vm/tests/events.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -99,8 +99,10 @@ fn helper_execute_epoch( ); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); - env.initialize_contract(contract_id.clone(), contract) + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); + exec_state + .initialize_contract(&invoke_ctx, contract_id.clone(), contract) .unwrap(); } diff --git a/stackslib/src/clarity_vm/tests/forking.rs b/stackslib/src/clarity_vm/tests/forking.rs index 0b3dcf9e9c1..ba64ee0f75a 100644 --- a/stackslib/src/clarity_vm/tests/forking.rs +++ b/stackslib/src/clarity_vm/tests/forking.rs @@ -82,9 +82,10 @@ fn test_at_block_mutations(#[case] version: ClarityVersion, #[case] epoch: Stack eprintln!("Branched execution..."); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); let command = "(var-get datum)"; - let value = env.eval_read_only(&c, command).unwrap(); + let value = exec_state.eval_read_only(&invoke_ctx, &c, command).unwrap(); assert_eq!(value, Value::Int(expected_value)); } @@ -98,27 +99,46 @@ fn test_at_block_mutations(#[case] version: ClarityVersion, #[case] epoch: Stack epoch, initialize, |x| { - assert_eq!( - branch(x, version, 1, "working").unwrap(), - Value::okay(Value::Int(1)).unwrap() - ); - assert_eq!( - branch(x, version, 1, "broken").unwrap(), - Value::okay(Value::Int(1)).unwrap() - ); - assert_eq!( - branch(x, version, 10, "working").unwrap(), - Value::okay(Value::Int(1)).unwrap() - ); - // make this test fail: this assertion _should_ be - // true, but at-block is broken. when a context - // switches to an at-block context, _any_ of the db - // wrapping that the Clarity VM does needs to be - // ignored. - assert_eq!( - branch(x, version, 10, "broken").unwrap(), - Value::okay(Value::Int(1)).unwrap() - ); + if epoch.supports_at_block() { + assert_eq!( + branch(x, version, 1, "working").unwrap(), + Value::okay(Value::Int(1)).unwrap() + ); + assert_eq!( + branch(x, version, 1, "broken").unwrap(), + Value::okay(Value::Int(1)).unwrap() + ); + assert_eq!( + branch(x, version, 10, "working").unwrap(), + Value::okay(Value::Int(1)).unwrap() + ); + // make this test fail: this assertion _should_ be + // true, but at-block is broken. when a context + // switches to an at-block context, _any_ of the db + // wrapping that the Clarity VM does needs to be + // ignored. + assert_eq!( + branch(x, version, 10, "broken").unwrap(), + Value::okay(Value::Int(1)).unwrap() + ); + } else { + assert_eq!( + branch(x, version, 1, "working").unwrap_err(), + RuntimeCheckErrorKind::AtBlockUnavailable.into() + ); + assert_eq!( + branch(x, version, 1, "broken").unwrap_err(), + RuntimeCheckErrorKind::AtBlockUnavailable.into() + ); + assert_eq!( + branch(x, version, 1, "working").unwrap_err(), + RuntimeCheckErrorKind::AtBlockUnavailable.into() + ); + assert_eq!( + branch(x, version, 1, "broken").unwrap_err(), + RuntimeCheckErrorKind::AtBlockUnavailable.into() + ); + } }, |_x| {}, |_x| {}, @@ -159,9 +179,10 @@ fn test_at_block_good(#[case] version: ClarityVersion, #[case] epoch: StacksEpoc eprintln!("Branched execution..."); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); let command = "(var-get datum)"; - let value = env.eval_read_only(&c, command).unwrap(); + let value = exec_state.eval_read_only(&invoke_ctx, &c, command).unwrap(); assert_eq!(value, Value::Int(expected_value)); } @@ -183,21 +204,32 @@ fn test_at_block_good(#[case] version: ClarityVersion, #[case] epoch: StacksEpoc |x| { let resp = branch(x, version, 1, "reset").unwrap_err(); eprintln!("{}", resp); - match resp { - VmExecutionError::Runtime(x, _) => assert_eq!( - x, - RuntimeError::UnknownBlockHeaderHash(BlockHeaderHash::from( - vec![2; 32].as_slice() - )) - ), - _ => panic!("Unexpected error"), + if epoch.supports_at_block() { + match resp { + VmExecutionError::Runtime(x, _) => assert_eq!( + x, + RuntimeError::UnknownBlockHeaderHash(BlockHeaderHash::from( + vec![2; 32].as_slice() + )) + ), + _ => panic!("Unexpected error"), + } + } else { + assert_eq!(resp, RuntimeCheckErrorKind::AtBlockUnavailable.into()); } }, |x| { - assert_eq!( - branch(x, version, 10, "reset").unwrap(), - Value::okay(Value::Int(11)).unwrap() - ); + if epoch.supports_at_block() { + assert_eq!( + branch(x, version, 10, "reset").unwrap(), + Value::okay(Value::Int(11)).unwrap() + ); + } else { + assert_eq!( + branch(x, version, 10, "reset").unwrap_err(), + RuntimeCheckErrorKind::AtBlockUnavailable.into() + ); + } }, ); } @@ -242,13 +274,17 @@ fn test_at_block_missing_defines(#[case] version: ClarityVersion, #[case] epoch: |_| {}, |env| { let err = initialize_2(env); - assert_eq!( - err, - RuntimeCheckErrorKind::NoSuchContract( - "S1G2081040G2081040G2081040G208105NK8PE5.contract-a".into() - ) - .into() - ); + if epoch.supports_at_block() { + assert_eq!( + err, + RuntimeCheckErrorKind::NoSuchContract( + "S1G2081040G2081040G2081040G208105NK8PE5.contract-a".into() + ) + .into() + ); + } else { + assert_eq!(err, RuntimeCheckErrorKind::AtBlockUnavailable.into()); + } }, ); } @@ -367,9 +403,12 @@ fn branched_execution( eprintln!("Branched execution..."); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); let command = format!("(get-balance {})", p1_str); - let balance = env.eval_read_only(&contract_identifier, &command).unwrap(); + let balance = exec_state + .eval_read_only(&invoke_ctx, &contract_identifier, &command) + .unwrap(); let expected = if expect_success { 10 } else { 0 }; assert_eq!(balance, Value::UInt(expected)); } diff --git a/stackslib/src/clarity_vm/tests/large_contract.rs b/stackslib/src/clarity_vm/tests/large_contract.rs index cef7968d4f0..3a9de226843 100644 --- a/stackslib/src/clarity_vm/tests/large_contract.rs +++ b/stackslib/src/clarity_vm/tests/large_contract.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -561,156 +561,179 @@ fn inner_test_simple_naming_system(owned_env: &mut OwnedEnvironment, version: Cl ); { - let mut env = owned_env.get_exec_environment(None, None, &placeholder_context); + let (mut exec_state, invoke_ctx) = + owned_env.get_exec_environment(None, None, &placeholder_context); let contract_identifier = QualifiedContractIdentifier::local("tokens").unwrap(); - env.initialize_contract(contract_identifier, tokens_contract) + exec_state + .initialize_contract(&invoke_ctx, contract_identifier, tokens_contract) .unwrap(); let contract_identifier = QualifiedContractIdentifier::local("names").unwrap(); - env.initialize_contract(contract_identifier, names_contract) + exec_state + .initialize_contract(&invoke_ctx, contract_identifier, names_contract) .unwrap(); } { - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p2.clone().expect_principal().unwrap()), None, &placeholder_context, ); assert!(is_err_code_i128( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "preorder", - &symbols_from_values(vec![name_hash_expensive_0.clone(), Value::UInt(1000)]), - false - ) - .unwrap(), + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "preorder", + &symbols_from_values(vec![name_hash_expensive_0.clone(), Value::UInt(1000)]), + false + ) + .unwrap(), 1 )); } { - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.clone().expect_principal().unwrap()), None, &placeholder_context, ); assert!(is_committed( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "preorder", - &symbols_from_values(vec![name_hash_expensive_0.clone(), Value::UInt(1000)]), - false - ) - .unwrap() + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "preorder", + &symbols_from_values(vec![name_hash_expensive_0.clone(), Value::UInt(1000)]), + false + ) + .unwrap() )); assert!(is_err_code_i128( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "preorder", - &symbols_from_values(vec![name_hash_expensive_0, Value::UInt(1000)]), - false - ) - .unwrap(), + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "preorder", + &symbols_from_values(vec![name_hash_expensive_0, Value::UInt(1000)]), + false + ) + .unwrap(), 2 )); } { // shouldn't be able to register a name you didn't preorder! - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p2.clone().expect_principal().unwrap()), None, &placeholder_context, ); assert!(is_err_code_i128( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "register", - &symbols_from_values(vec![p2.clone(), Value::Int(1), Value::Int(0)]), - false - ) - .unwrap(), + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "register", + &symbols_from_values(vec![p2.clone(), Value::Int(1), Value::Int(0)]), + false + ) + .unwrap(), 4 )); } { // should work! - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p1.expect_principal().unwrap()), None, &placeholder_context, ); assert!(is_committed( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "register", - &symbols_from_values(vec![p2.clone(), Value::Int(1), Value::Int(0)]), - false - ) - .unwrap() + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "register", + &symbols_from_values(vec![p2.clone(), Value::Int(1), Value::Int(0)]), + false + ) + .unwrap() )); } { // try to underpay! - let mut env = owned_env.get_exec_environment( + let (mut exec_state, invoke_ctx) = owned_env.get_exec_environment( Some(p2.clone().expect_principal().unwrap()), None, &placeholder_context, ); assert!(is_committed( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "preorder", - &symbols_from_values(vec![name_hash_expensive_1, Value::UInt(100)]), - false - ) - .unwrap() + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "preorder", + &symbols_from_values(vec![name_hash_expensive_1, Value::UInt(100)]), + false + ) + .unwrap() )); assert!(is_err_code_i128( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "register", - &symbols_from_values(vec![p2.clone(), Value::Int(2), Value::Int(0)]), - false - ) - .unwrap(), + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "register", + &symbols_from_values(vec![p2.clone(), Value::Int(2), Value::Int(0)]), + false + ) + .unwrap(), 4 )); // register a cheap name! assert!(is_committed( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "preorder", - &symbols_from_values(vec![name_hash_cheap_0, Value::UInt(100)]), - false - ) - .unwrap() + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "preorder", + &symbols_from_values(vec![name_hash_cheap_0, Value::UInt(100)]), + false + ) + .unwrap() )); assert!(is_committed( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "register", - &symbols_from_values(vec![p2.clone(), Value::Int(100001), Value::Int(0)]), - false - ) - .unwrap() + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "register", + &symbols_from_values(vec![p2.clone(), Value::Int(100001), Value::Int(0)]), + false + ) + .unwrap() )); // preorder must exist! assert!(is_err_code_i128( - &env.execute_contract( - &QualifiedContractIdentifier::local("names").unwrap(), - "register", - &symbols_from_values(vec![p2, Value::Int(100001), Value::Int(0)]), - false - ) - .unwrap(), + &exec_state + .execute_contract( + &invoke_ctx, + &QualifiedContractIdentifier::local("names").unwrap(), + "register", + &symbols_from_values(vec![p2, Value::Int(100001), Value::Int(0)]), + false + ) + .unwrap(), 5 )); } diff --git a/stackslib/src/clarity_vm/tests/smoke.rs b/stackslib/src/clarity_vm/tests/smoke.rs index 37998f5d06e..251ede71b01 100644 --- a/stackslib/src/clarity_vm/tests/smoke.rs +++ b/stackslib/src/clarity_vm/tests/smoke.rs @@ -24,12 +24,12 @@ use crate::chainstate::stacks::index::ClarityMarfTrieId; use crate::clarity_vm::clarity::{ClarityMarfStore, ClarityMarfStoreTransaction}; use crate::clarity_vm::database::marf::MarfedKV; -pub fn with_marfed_environment(f: F, top_level: bool) +pub fn with_marfed_environment(f: F, top_level: bool, epoch: Option) where F: FnOnce(&mut OwnedEnvironment), { let mut marf_kv = MarfedKV::temporary(); - + let epoch = epoch.unwrap_or(StacksEpochId::latest()); { let mut store = marf_kv.begin( &StacksBlockId::sentinel(), @@ -50,7 +50,7 @@ where let mut owned_env = OwnedEnvironment::new( store.as_clarity_db(&TEST_HEADER_DB, &TEST_BURN_STATE_DB), - StacksEpochId::latest(), + epoch, ); // start an initial transaction. if !top_level { @@ -84,5 +84,5 @@ fn test_at_unknown_block() { } } - with_marfed_environment(test, true); + with_marfed_environment(test, true, Some(StacksEpochId::Epoch33)); } diff --git a/stackslib/src/config/mod.rs b/stackslib/src/config/mod.rs index a423577bcee..08b84d3f189 100644 --- a/stackslib/src/config/mod.rs +++ b/stackslib/src/config/mod.rs @@ -2544,12 +2544,19 @@ impl NodeConfig { if let Some(addr) = addrs.next() { break addr; } else { - panic!("No addresses found for '{hostport}'"); + error!("No addresses found for bootstrap node, skipping"; "host" => hostport); + return; } } Err(e) => { if attempts >= max_attempts { - panic!("Failed to resolve '{hostport}' after {max_attempts} attempts: {e}"); + error!( + "Failed to resolve bootstrap node, skipping"; + "host" => hostport, + "attempts" => max_attempts, + "error" => %e + ); + return; } else { error!( "Attempt {} - Failed to resolve '{hostport}': {e}. Retrying in {delay:?}...", @@ -2581,7 +2588,19 @@ impl NodeConfig { } pub fn add_deny_node(&mut self, deny_node: &str, chain_id: u32, peer_version: u32) { - let sockaddr = deny_node.to_socket_addrs().unwrap().next().unwrap(); + let sockaddr = match deny_node.to_socket_addrs() { + Ok(mut addrs) => match addrs.next() { + Some(addr) => addr, + None => { + error!("No addresses found for deny node, skipping"; "node" => deny_node); + return; + } + }, + Err(e) => { + error!("Failed to resolve deny node, skipping"; "node" => deny_node, "error" => %e); + return; + } + }; let neighbor = NodeConfig::default_neighbor( sockaddr, Secp256k1PublicKey::from_private(&Secp256k1PrivateKey::random()), @@ -3875,7 +3894,8 @@ pub struct NodeConfigFile { impl NodeConfigFile { fn into_config_default(self, default_node_config: NodeConfig) -> Result { - let rpc_bind = self.rpc_bind.unwrap_or(default_node_config.rpc_bind); + let rpc_bind = std::env::var("STACKS_RPC_BIND") + .unwrap_or(self.rpc_bind.unwrap_or(default_node_config.rpc_bind)); let miner = self.miner.unwrap_or(default_node_config.miner); let stacker = self.stacker.unwrap_or(default_node_config.stacker); let node_config = NodeConfig { @@ -3888,7 +3908,8 @@ impl NodeConfigFile { working_dir: std::env::var("STACKS_WORKING_DIR") .unwrap_or(self.working_dir.unwrap_or(default_node_config.working_dir)), rpc_bind: rpc_bind.clone(), - p2p_bind: self.p2p_bind.unwrap_or(default_node_config.p2p_bind), + p2p_bind: std::env::var("STACKS_P2P_BIND") + .unwrap_or(self.p2p_bind.unwrap_or(default_node_config.p2p_bind)), p2p_address: self.p2p_address.unwrap_or(rpc_bind.clone()), bootstrap_node: vec![], deny_nodes: vec![], @@ -3930,7 +3951,9 @@ impl NodeConfigFile { next_initiative_delay: self .next_initiative_delay .unwrap_or(default_node_config.next_initiative_delay), - prometheus_bind: self.prometheus_bind, + prometheus_bind: std::env::var("STACKS_PROMETHEUS_BIND") + .ok() + .or(self.prometheus_bind), marf_cache_strategy: self.marf_cache_strategy, marf_defer_hashing: self .marf_defer_hashing diff --git a/stackslib/src/core/mod.rs b/stackslib/src/core/mod.rs index 7e0c2bac284..45e23861352 100644 --- a/stackslib/src/core/mod.rs +++ b/stackslib/src/core/mod.rs @@ -114,8 +114,8 @@ pub const BITCOIN_MAINNET_STACKS_31_BURN_HEIGHT: u64 = 875_000; pub const BITCOIN_MAINNET_STACKS_32_BURN_HEIGHT: u64 = 907_740; /// This is Epoch-3.3, activation timing proposed in SIP-033 pub const BITCOIN_MAINNET_STACKS_33_BURN_HEIGHT: u64 = 923_222; -/// This is Epoch-3.4, activation timing will be proposed in a future SIP -pub const BITCOIN_MAINNET_STACKS_34_BURN_HEIGHT: u64 = 3_400_000; +/// This is Epoch-3.4, activation timing proposed in SIP-039 +pub const BITCOIN_MAINNET_STACKS_34_BURN_HEIGHT: u64 = 943_333; /// Bitcoin mainline testnet3 activation heights. /// TODO: No longer used since testnet3 is dead, so remove. diff --git a/stackslib/src/net/api/blockreplay.rs b/stackslib/src/net/api/blockreplay.rs index 842c8a40e47..e6d4a91ce22 100644 --- a/stackslib/src/net/api/blockreplay.rs +++ b/stackslib/src/net/api/blockreplay.rs @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Stacks Open Internet Foundation +// Copyright (C) 2025-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -403,20 +403,20 @@ impl RPCReplayedBlockTransaction { receipt: &StacksTransactionReceipt, profiler_result: &BlockReplayProfilerResult, ) -> Self { - let events = receipt - .events - .iter() - .enumerate() - .map(|(event_index, event)| { - event - .json_serialize( - event_index, - &receipt.transaction.txid(), - !receipt.post_condition_aborted, - ) - .unwrap() - }) - .collect(); + let events = if receipt.post_condition_aborted { + vec![] + } else { + receipt + .events + .iter() + .enumerate() + .map(|(event_index, event)| { + event + .json_serialize(event_index, &receipt.transaction.txid(), true) + .unwrap() + }) + .collect() + }; let transaction_data = match &receipt.transaction { TransactionOrigin::Stacks(stacks) => Some(stacks.clone()), diff --git a/stackslib/src/net/api/callreadonly.rs b/stackslib/src/net/api/callreadonly.rs index 5cf65bd0e97..168c7b47826 100644 --- a/stackslib/src/net/api/callreadonly.rs +++ b/stackslib/src/net/api/callreadonly.rs @@ -235,20 +235,22 @@ impl RPCRequestHandler for RPCCallReadOnlyRequestHandler { sender, sponsor, cost_track, - |env| { + |exec_state, invoke_ctx| { // we want to execute any function as long as no actual writes are made as // opposed to be limited to purely calling `define-read-only` functions, // so use `read_only = false`. This broadens the number of functions that // can be called, and also circumvents limitations on `define-read-only` // functions that can not use `contrac-call?`, even when calling other // read-only functions - env.execute_contract( - &contract_identifier, - function.as_str(), - &args, - false, - ) - .map_err(ClarityEvalError::from) + exec_state + .execute_contract( + invoke_ctx, + &contract_identifier, + function.as_str(), + &args, + false, + ) + .map_err(ClarityEvalError::from) }, ) }, diff --git a/stackslib/src/net/api/fastcallreadonly.rs b/stackslib/src/net/api/fastcallreadonly.rs index 50b7d28cefa..af8b45f203b 100644 --- a/stackslib/src/net/api/fastcallreadonly.rs +++ b/stackslib/src/net/api/fastcallreadonly.rs @@ -228,11 +228,12 @@ impl RPCRequestHandler for RPCFastCallReadOnlyRequestHandler { sender, sponsor, LimitedCostTracker::new_free(), - |env| { + |exec_state, invoke_ctx| { // cost tracking in read only calls is meamingful mainly from a security point of view // for this reason we enforce max_execution_time when cost tracking is disabled/free - env.global_context + exec_state + .global_context .set_max_execution_time(self.read_only_max_execution_time); // we want to execute any function as long as no actual writes are made as @@ -241,13 +242,15 @@ impl RPCRequestHandler for RPCFastCallReadOnlyRequestHandler { // can be called, and also circumvents limitations on `define-read-only` // functions that can not use `contrac-call?`, even when calling other // read-only functions - env.execute_contract( - &contract_identifier, - function.as_str(), - &args, - false, - ) - .map_err(ClarityEvalError::from) + exec_state + .execute_contract( + invoke_ctx, + &contract_identifier, + function.as_str(), + &args, + false, + ) + .map_err(ClarityEvalError::from) }, ) }, diff --git a/stackslib/src/net/api/getpoxinfo.rs b/stackslib/src/net/api/getpoxinfo.rs index 8554492cf25..7dac63c2ea0 100644 --- a/stackslib/src/net/api/getpoxinfo.rs +++ b/stackslib/src/net/api/getpoxinfo.rs @@ -200,8 +200,15 @@ impl RPCPoxInfoData { sender, None, cost_track, - |env| { - env.execute_contract(&contract_identifier, function, &[], true) + |exec_state, invoke_ctx| { + exec_state + .execute_contract( + invoke_ctx, + &contract_identifier, + function, + &[], + true, + ) .map_err(ClarityEvalError::from) }, ) diff --git a/stackslib/src/net/api/getsigner.rs b/stackslib/src/net/api/getsigner.rs index adff8e89323..e80923ba363 100644 --- a/stackslib/src/net/api/getsigner.rs +++ b/stackslib/src/net/api/getsigner.rs @@ -21,10 +21,8 @@ use crate::net::http::{ parse_json, Error, HttpNotFound, HttpRequest, HttpRequestContents, HttpRequestPreamble, HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble, }; -use crate::net::httpcore::{ - HttpRequestContentsExtensions as _, RPCRequestHandler, StacksHttpRequest, StacksHttpResponse, -}; -use crate::net::{Error as NetError, StacksNodeState, TipRequest}; +use crate::net::httpcore::{RPCRequestHandler, StacksHttpRequest, StacksHttpResponse}; +use crate::net::{Error as NetError, StacksNodeState}; #[derive(Clone, Default)] pub struct GetSignerRequestHandler { @@ -171,13 +169,12 @@ impl StacksHttpRequest { host: PeerHost, signer_pubkey: &Secp256k1PublicKey, cycle_num: u64, - tip_req: TipRequest, ) -> StacksHttpRequest { StacksHttpRequest::new_for_peer( host, "GET".into(), format!("/v3/signer/{}/{cycle_num}", signer_pubkey.to_hex()), - HttpRequestContents::new().for_tip(tip_req), + HttpRequestContents::new(), ) .expect("FATAL: failed to construct request from infallible data") } diff --git a/stackslib/src/net/api/tests/blockreplay.rs b/stackslib/src/net/api/tests/blockreplay.rs index b012691dc07..7658253716b 100644 --- a/stackslib/src/net/api/tests/blockreplay.rs +++ b/stackslib/src/net/api/tests/blockreplay.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2025 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -17,7 +17,7 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use clarity::types::chainstate::StacksPrivateKey; -use clarity::vm::{ClarityName, ContractName, Value as ClarityValue}; +use clarity::vm::{ClarityName, ContractName}; use stacks_common::consts::CHAIN_ID_TESTNET; use stacks_common::types::chainstate::StacksBlockId; @@ -197,7 +197,12 @@ fn test_try_make_response() { for (resp_tx, tip_tx) in resp.transactions.iter().zip(tip_block.receipts.iter()) { assert_eq!(resp_tx.txid, tip_tx.transaction.txid()); - assert_eq!(resp_tx.events.len(), tip_tx.events.len()); + let expected_events = if resp_tx.post_condition_aborted { + 0 + } else { + tip_tx.events.len() + }; + assert_eq!(resp_tx.events.len(), expected_events); assert_eq!(resp_tx.result, tip_tx.result); assert_eq!(resp_tx.result_hex, tip_tx.result); assert!(!resp_tx.post_condition_aborted); @@ -224,8 +229,7 @@ fn test_try_make_response() { assert_eq!(preamble.status_code, 401); } -/// Test that events properly set the `committed` flag to `false` -/// when the transaction is aborted by a post-condition. +/// Test that post-condition aborted transactions are included in the response but with empty events. #[test] fn replay_block_with_pc_failure() { let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333); @@ -290,8 +294,6 @@ fn replay_block_with_pc_failure() { .with_initial_balances(vec![(addr.into(), 1_000_000)]) }); - let nakamoto_consensus_hash = rpc_test.consensus_hash.clone(); - let mut requests = vec![]; let mut request = @@ -308,43 +310,20 @@ fn replay_block_with_pc_failure() { std::str::from_utf8(&response.try_serialize().unwrap()).unwrap() ); - let contents = response.clone().get_http_payload_ok().unwrap(); - let response_json: serde_json::Value = contents.try_into().unwrap(); - - let result_hex = response_json - .get("transactions") - .expect("Expected JSON to have a transactions field") - .as_array() - .expect("Expected transactions to be an array") - .get(0) - .expect("Expected transactions to have at least one element") - .as_object() - .expect("Expected transaction to be an object") - .get("result_hex") - .expect("Expected JSON to have a result_hex field") - .as_str() - .unwrap(); - let result = ClarityValue::try_deserialize_hex_untyped(&result_hex).unwrap(); - result.expect_result_ok().expect("FATAL: result is not ok"); - let resp = response.decode_replayed_block().unwrap(); - let tip_block = test_observer.get_blocks().last().unwrap().clone(); - - assert_eq!(resp.transactions.len(), tip_block.receipts.len()); - - assert_eq!(resp.transactions.len(), 1); - - let resp_tx = &resp.transactions.get(0).unwrap(); - - assert!(resp_tx.vm_error.is_some()); - - for event in resp_tx.events.iter() { - let committed = event.get("committed").unwrap().as_bool().unwrap(); - assert!(!committed); - } - - assert!(resp_tx.post_condition_aborted); + // The post-condition aborted transaction should be present in the response (for fee + // reconciliation), but its events should be empty. + let aborted_tx = resp + .transactions + .iter() + .find(|tx| tx.post_condition_aborted) + .expect("Expected to find a post-condition aborted transaction in the response"); + + assert!( + aborted_tx.events.is_empty(), + "Expected the post-condition aborted transaction to have empty events" + ); } #[test] @@ -416,7 +395,12 @@ fn test_try_make_response_with_unsuccessful_transaction() { for (resp_tx, tip_tx) in resp.transactions.iter().zip(tip_block.receipts.iter()) { assert_eq!(resp_tx.txid, tip_tx.transaction.txid()); - assert_eq!(resp_tx.events.len(), tip_tx.events.len()); + let expected_events = if resp_tx.post_condition_aborted { + 0 + } else { + tip_tx.events.len() + }; + assert_eq!(resp_tx.events.len(), expected_events); assert_eq!(resp_tx.result, tip_tx.result); assert_eq!(resp_tx.result_hex, tip_tx.result); assert!(!resp_tx.post_condition_aborted); diff --git a/stackslib/src/net/api/tests/blocksimulate.rs b/stackslib/src/net/api/tests/blocksimulate.rs index 89c58afaa0c..ddf2b99d3d2 100644 --- a/stackslib/src/net/api/tests/blocksimulate.rs +++ b/stackslib/src/net/api/tests/blocksimulate.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2025 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -18,7 +18,7 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use clarity::types::chainstate::StacksPrivateKey; use clarity::vm::types::PrincipalData; -use clarity::vm::{ClarityName, ContractName, Value as ClarityValue}; +use clarity::vm::{ClarityName, ContractName}; use stacks_common::consts::CHAIN_ID_TESTNET; use stacks_common::types::chainstate::StacksBlockId; @@ -275,8 +275,7 @@ fn test_try_make_response() { assert_eq!(preamble.status_code, 401); } -/// Test that events properly set the `committed` flag to `false` -/// when the transaction is aborted by a post-condition. +/// Test that post-condition aborted transactions are included in the response but with empty events. #[test] fn simulate_block_with_pc_failure() { let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333); @@ -352,8 +351,6 @@ fn simulate_block_with_pc_failure() { tx_signer.get_tx().unwrap() }; - let nakamoto_consensus_hash = rpc_test.consensus_hash.clone(); - let mut requests = vec![]; let mut request = StacksHttpRequest::new_block_simulate( @@ -374,43 +371,20 @@ fn simulate_block_with_pc_failure() { std::str::from_utf8(&response.try_serialize().unwrap()).unwrap() ); - let contents = response.clone().get_http_payload_ok().unwrap(); - let response_json: serde_json::Value = contents.try_into().unwrap(); - - let result_hex = response_json - .get("transactions") - .expect("Expected JSON to have a transactions field") - .as_array() - .expect("Expected transactions to be an array") - .get(0) - .expect("Expected transactions to have at least one element") - .as_object() - .expect("Expected transaction to be an object") - .get("result_hex") - .expect("Expected JSON to have a result_hex field") - .as_str() - .unwrap(); - let result = ClarityValue::try_deserialize_hex_untyped(&result_hex).unwrap(); - result.expect_result_ok().expect("FATAL: result is not ok"); - let resp = response.decode_simulated_block().unwrap(); - let tip_block = test_observer.get_blocks().last().unwrap().clone(); - - assert_eq!(resp.transactions.len(), tip_block.receipts.len()); - - assert_eq!(resp.transactions.len(), 1); - - let resp_tx = &resp.transactions.get(0).unwrap(); - - assert!(resp_tx.vm_error.is_some()); - - for event in resp_tx.events.iter() { - let committed = event.get("committed").unwrap().as_bool().unwrap(); - assert!(!committed); - } - - assert!(resp_tx.post_condition_aborted); + // The post-condition aborted transaction should be present in the response (for fee + // reconciliation), but its events should be empty. + let aborted_tx = resp + .transactions + .iter() + .find(|tx| tx.post_condition_aborted) + .expect("Expected to find a post-condition aborted transaction in the response"); + + assert!( + aborted_tx.events.is_empty(), + "Expected the post-condition aborted transaction to have empty events" + ); } #[test] diff --git a/stackslib/src/net/api/tests/getsigner.rs b/stackslib/src/net/api/tests/getsigner.rs index bf879b84305..b25edbc9201 100644 --- a/stackslib/src/net/api/tests/getsigner.rs +++ b/stackslib/src/net/api/tests/getsigner.rs @@ -24,7 +24,7 @@ use crate::net::api::getsigner; use crate::net::api::tests::TestRPC; use crate::net::connection::ConnectionOptions; use crate::net::http::{Error as HttpError, HttpRequestPreamble, HttpVersion}; -use crate::net::httpcore::{RPCRequestHandler, StacksHttp, StacksHttpRequest, TipRequest}; +use crate::net::httpcore::{RPCRequestHandler, StacksHttp, StacksHttpRequest}; use crate::net::test::TestEventObserver; use crate::net::Error as NetError; @@ -109,26 +109,14 @@ fn test_try_make_response() { let random_private_key = StacksPrivateKey::random(); let random_public_key = StacksPublicKey::from_private(&random_private_key); - let nakamoto_chain_tip = rpc_test.canonical_tip.clone(); - let mut requests = vec![]; // Query existing signer - let info = StacksHttpRequest::new_getsigner( - addr.into(), - &public_key, - cycle_num, - TipRequest::SpecificTip(nakamoto_chain_tip.clone()), - ); + let info = StacksHttpRequest::new_getsigner(addr.into(), &public_key, cycle_num); requests.push(info); // query random signer that doesn't exist - let request = StacksHttpRequest::new_getsigner( - addr.into(), - &random_public_key, - cycle_num, - TipRequest::SpecificTip(nakamoto_chain_tip), - ); + let request = StacksHttpRequest::new_getsigner(addr.into(), &random_public_key, cycle_num); requests.push(request); let mut responses = rpc_test.run(requests); diff --git a/stackslib/src/net/chat.rs b/stackslib/src/net/chat.rs index 84bb94dbc8e..01cf910ab8f 100644 --- a/stackslib/src/net/chat.rs +++ b/stackslib/src/net/chat.rs @@ -2090,7 +2090,12 @@ impl ConversationP2P { preamble: &Preamble, relayers: Vec, ) -> Result, net_error> { - assert!(preamble.payload_len > 5); // don't count 1-byte type prefix + 4 byte vector length + if preamble.payload_len <= 5 { + return Err(net_error::DeserializeError(format!( + "Blocks push payload len is too small: {}", + preamble.payload_len + ))); + } let local_peer = network.get_local_peer(); let chain_view = network.get_chain_view(); @@ -2130,7 +2135,12 @@ impl ConversationP2P { preamble: &Preamble, relayers: Vec, ) -> Result, net_error> { - assert!(preamble.payload_len > 5); // don't count 1-byte type prefix + 4 byte vector length + if preamble.payload_len <= 5 { + return Err(net_error::DeserializeError(format!( + "Microblocks push payload len is too small: {}", + preamble.payload_len + ))); + } let local_peer = network.get_local_peer(); let chain_view = network.get_chain_view(); @@ -2167,7 +2177,12 @@ impl ConversationP2P { preamble: &Preamble, relayers: Vec, ) -> Result, net_error> { - assert!(preamble.payload_len > 1); // don't count 1-byte type prefix + if preamble.payload_len <= 1 { + return Err(net_error::DeserializeError(format!( + "Transaction push payload len is too small: {}", + preamble.payload_len + ))); + } let local_peer = network.get_local_peer(); let chain_view = network.get_chain_view(); @@ -2205,7 +2220,12 @@ impl ConversationP2P { preamble: &Preamble, relayers: Vec, ) -> Result, net_error> { - assert!(preamble.payload_len > 1); // don't count 1-byte type prefix + if preamble.payload_len <= 1 { + return Err(net_error::DeserializeError(format!( + "StackerDB push payload len is too small: {}", + preamble.payload_len + ))); + } let local_peer = network.get_local_peer(); let chain_view = network.get_chain_view(); @@ -2244,7 +2264,12 @@ impl ConversationP2P { preamble: &Preamble, relayers: Vec, ) -> Result, net_error> { - assert!(preamble.payload_len > 1); // don't count 1-byte type prefix + if preamble.payload_len <= 1 { + return Err(net_error::DeserializeError(format!( + "Nakamoto blocks push payload len is too small: {}", + preamble.payload_len + ))); + } let local_peer = network.get_local_peer(); let chain_view = network.get_chain_view(); @@ -6948,6 +6973,21 @@ mod test { ); // NOTE: payload can be anything since we only look at premable length here + let payload = StacksMessageType::Nack(NackData { error_code: 123 }); + let mut short_msg = convo_1 + .sign_relay_message(&local_peer_1, &chain_view, vec![], payload) + .unwrap(); + + short_msg.preamble.payload_len = 5; + + let fail = convo_1 + .validate_blocks_push(&net_1, &short_msg.preamble, short_msg.relayers.clone()) + .unwrap_err(); + assert!( + matches!(fail, net_error::DeserializeError(_)), + "Wrong error {fail:?}" + ); + let payload = StacksMessageType::Nack(NackData { error_code: 123 }); // bad message -- got bad relayers (cycle) @@ -7075,6 +7115,21 @@ mod test { ); // NOTE: payload can be anything since we only look at premable length here + let payload = StacksMessageType::Nack(NackData { error_code: 123 }); + let mut short_msg = convo_1 + .sign_relay_message(&local_peer_1, &chain_view, vec![], payload) + .unwrap(); + + short_msg.preamble.payload_len = 1; + + let fail = convo_1 + .validate_transaction_push(&net_1, &short_msg.preamble, short_msg.relayers.clone()) + .unwrap_err(); + assert!( + matches!(fail, net_error::DeserializeError(_)), + "Wrong error {fail:?}" + ); + let payload = StacksMessageType::Nack(NackData { error_code: 123 }); // bad message -- got bad relayers (cycle) @@ -7202,6 +7257,21 @@ mod test { ); // NOTE: payload can be anything since we only look at premable length here + let payload = StacksMessageType::Nack(NackData { error_code: 123 }); + let mut short_msg = convo_1 + .sign_relay_message(&local_peer_1, &chain_view, vec![], payload) + .unwrap(); + + short_msg.preamble.payload_len = 5; + + let fail = convo_1 + .validate_microblocks_push(&net_1, &short_msg.preamble, short_msg.relayers.clone()) + .unwrap_err(); + assert!( + matches!(fail, net_error::DeserializeError(_)), + "Wrong error {fail:?}" + ); + let payload = StacksMessageType::Nack(NackData { error_code: 123 }); // bad message -- got bad relayers (cycle) @@ -7329,6 +7399,21 @@ mod test { ); // NOTE: payload can be anything since we only look at premable length here + let payload = StacksMessageType::Nack(NackData { error_code: 123 }); + let mut short_msg = convo_1 + .sign_relay_message(&local_peer_1, &chain_view, vec![], payload) + .unwrap(); + + short_msg.preamble.payload_len = 1; + + let fail = convo_1 + .validate_stackerdb_push(&net_1, &short_msg.preamble, short_msg.relayers.clone()) + .unwrap_err(); + assert!( + matches!(fail, net_error::DeserializeError(_)), + "Wrong error {fail:?}" + ); + let payload = StacksMessageType::Nack(NackData { error_code: 123 }); // bad message -- got bad relayers (cycle) @@ -7395,4 +7480,80 @@ mod test { .is_some()); assert_eq!(convo_1.stats.msgs_err, err_before); } + + #[test] + fn test_validate_nakamoto_block_push_invalid_payload_len() { + let mut conn_opts = ConnectionOptions::default(); + conn_opts.max_nakamoto_block_push_bandwidth = 100; + + let socketaddr_1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 8081); + + let first_burn_hash = BurnchainHeaderHash::from_hex( + "0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(); + + let mut chain_view = BurnchainView { + burn_block_height: 12348, + burn_block_hash: BurnchainHeaderHash([0x11; 32]), + burn_stable_block_height: 12341, + burn_stable_block_hash: BurnchainHeaderHash([0x22; 32]), + last_burn_block_hashes: HashMap::new(), + rc_consensus_hash: ConsensusHash([0x33; 20]), + }; + chain_view.make_test_data(); + + let test_name_1 = "validate_nakamoto_block_push_invalid_payload_len_1"; + let burnchain = testing_burnchain_config(test_name_1); + + let (mut peerdb_1, mut sortdb_1, stackerdbs_1, pox_id_1, _) = make_test_chain_dbs( + test_name_1, + &burnchain, + 0x9abcdef0, + 12352, + "http://peer1.com".into(), + &[], + &[], + DEFAULT_SERVICES, + ); + + let net_1 = db_setup( + test_name_1, + &burnchain, + 0x9abcdef0, + &mut peerdb_1, + &mut sortdb_1, + &socketaddr_1, + &chain_view, + ); + + let local_peer_1 = PeerDB::get_local_peer(peerdb_1.conn()).unwrap(); + + let mut convo_1 = ConversationP2P::new( + 123, + 456, + &burnchain, + &socketaddr_1, + &conn_opts, + true, + 0, + StacksEpoch::unit_test_pre_2_05(0), + ); + + // NOTE: payload can be anything since we only look at premable length here + let payload = StacksMessageType::Nack(NackData { error_code: 123 }); + let mut short_msg = convo_1 + .sign_relay_message(&local_peer_1, &chain_view, vec![], payload) + .unwrap(); + + short_msg.preamble.payload_len = 1; + + let fail = convo_1 + .validate_nakamoto_block_push(&net_1, &short_msg.preamble, short_msg.relayers.clone()) + .unwrap_err(); + assert!( + matches!(fail, net_error::DeserializeError(_)), + "Wrong error {fail:?}" + ); + } } diff --git a/stackslib/src/net/tests/convergence.rs b/stackslib/src/net/tests/convergence.rs index 8440954eff6..199dcd6ca4f 100644 --- a/stackslib/src/net/tests/convergence.rs +++ b/stackslib/src/net/tests/convergence.rs @@ -164,7 +164,7 @@ fn test_walk_ring_15_plain() { #[ignore] fn test_walk_ring_15_pingback() { setup_rlimit_nofiles(); - with_timeout(600, || { + with_timeout(900, || { // initial peers are neither white- nor denied let mut peer_configs = vec![]; let peer_count: usize = 15; diff --git a/stackslib/src/util_lib/bloom.rs b/stackslib/src/util_lib/bloom.rs index f5f8873a818..fbc96c05c93 100644 --- a/stackslib/src/util_lib/bloom.rs +++ b/stackslib/src/util_lib/bloom.rs @@ -78,6 +78,10 @@ enum BitFieldEncoding { Full = 0x02, } +fn should_use_sparse_encoding(non_zero_bytes: usize, total_bytes: usize) -> bool { + non_zero_bytes * 5 + 4 < total_bytes +} + /// Encode the inner count array, using a sparse representation if it would save space fn encode_bitfield(fd: &mut W, bytes: &[u8]) -> Result<(), codec_error> { let mut num_filled = 0; @@ -87,7 +91,7 @@ fn encode_bitfield(fd: &mut W, bytes: &[u8]) -> Result<(), codec_error } } - if num_filled * 5 + 4 < bytes.len() { + if should_use_sparse_encoding(num_filled, bytes.len()) { // more efficient to encode as (4-byte-index, 1-byte-value) pairs, with an extra 4-byte header write_next(fd, &(BitFieldEncoding::Sparse as u8))?; write_next(fd, &(bytes.len() as u32))?; @@ -119,6 +123,12 @@ fn decode_bitfield(fd: &mut R) -> Result, codec_error> { } let num_filled: u32 = read_next(fd)?; + if !should_use_sparse_encoding(num_filled as usize, vec_len as usize) { + return Err(codec_error::OverflowError( + "Non-sparse bitfield should not use sparse encoding.".into(), + )); + } + let mut ret = vec![0u8; vec_len as usize]; for _ in 0..num_filled { let idx: u32 = read_next(fd)?; @@ -959,6 +969,45 @@ pub mod test { } } + #[test] + fn test_bloom_bitfield_sparse_threshold() { + // a bitfield that has only two bits set, one each in the first two bytes + let bitfield = BitField( + vec![ + 0x01, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ], + 256, + ); + let mut bytes = bitfield.serialize_to_vec(); + + // 4 for bit count, 1 for the encoding marker, 4 for byte length, + // 4 for `num_filled`, two times 5 for index/byte pairs + assert_eq!(bytes.len(), 23); + + assert_eq!(bytes[4], BitFieldEncoding::Sparse as u8); + + // Change the bit count in the serialization to 16. Technically that is + // still a valid representation, but at that size, it should've been + // serialized using full, not sparse, encoding. + bytes[2] = 0; // bit count 2nd-LSB + bytes[3] = 16; // bit count LSB + bytes[8] = 2; // byte length LSB + + let result = BitField::consensus_deserialize(&mut &bytes[..]); + + if let Err(codec_error::OverflowError(message)) = result { + assert_eq!( + message, + "Non-sparse bitfield should not use sparse encoding." + ); + } else { + error!("Unexpected BitField::consensus_deserialize result: {result:?}"); + panic!(); + } + } + #[test] fn test_bloom_filter_codec() { let num_items = 8192; diff --git a/stackslib/src/util_lib/db.rs b/stackslib/src/util_lib/db.rs index e6d61d6e616..afeff1c355d 100644 --- a/stackslib/src/util_lib/db.rs +++ b/stackslib/src/util_lib/db.rs @@ -39,8 +39,8 @@ use crate::chainstate::stacks::index::{Error as MARFError, MARFValue, MarfTrieId pub type DBConn = rusqlite::Connection; pub type DBTx<'a> = rusqlite::Transaction<'a>; -// 256MB -pub const SQLITE_MMAP_SIZE: i64 = 256 * 1024 * 1024; +// 1GB for MARF databases (state trie lookups benefit from larger mmap) +pub const SQLITE_MMAP_SIZE: i64 = 1024 * 1024 * 1024; // 32K pub const SQLITE_MARF_PAGE_SIZE: i64 = 32768; @@ -738,6 +738,9 @@ pub fn sqlite_open>( db.busy_handler(Some(tx_busy_handler))?; inner_sql_pragma(&db, "journal_mode", &"WAL")?; inner_sql_pragma(&db, "synchronous", &"NORMAL")?; + inner_sql_pragma(&db, "mmap_size", &(256 * 1024 * 1024))?; + inner_sql_pragma(&db, "cache_size", &(-32000))?; + inner_sql_pragma(&db, "wal_autocheckpoint", &500)?; if foreign_keys { inner_sql_pragma(&db, "foreign_keys", &true)?; } diff --git a/stackslib/src/util_lib/signed_structured_data.rs b/stackslib/src/util_lib/signed_structured_data.rs index a9e1f0e92ff..a6cf0409d8e 100644 --- a/stackslib/src/util_lib/signed_structured_data.rs +++ b/stackslib/src/util_lib/signed_structured_data.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2021 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -197,7 +197,7 @@ pub mod pox4 { sender.clone(), None, LimitedCostTracker::new_free(), - |env| { + |exec_state, invoke_ctx| { let program = format!( "(get-signer-key-message-hash {} u{} \"{}\" u{} u{} u{})", Value::Tuple(pox_addr.clone().as_clarity_tuple().unwrap()), //p @@ -207,7 +207,7 @@ pub mod pox4 { max_amount, auth_id, ); - env.eval_read_only(&pox_contract_id, &program) + exec_state.eval_read_only(invoke_ctx, &pox_contract_id, &program) }, ); result diff --git a/versions.toml b/versions.toml index 14357dfad0c..6a8595e9837 100644 --- a/versions.toml +++ b/versions.toml @@ -1,4 +1,4 @@ # Update these values when a new release is created. # `stacks-common/build.rs` will automatically update `versions.rs` with these values. -stacks_node_version = "3.3.0.0.6" -stacks_signer_version = "3.3.0.0.6.0" +stacks_node_version = "3.4.0.0.0" +stacks_signer_version = "3.4.0.0.0.0"