diff --git a/.github/workflows/test-write-program-buffer.yaml b/.github/workflows/test-write-program-buffer.yaml new file mode 100644 index 0000000..667a064 --- /dev/null +++ b/.github/workflows/test-write-program-buffer.yaml @@ -0,0 +1,72 @@ +name: Test write-program-buffer + +on: + pull_request: + paths: + - "write-program-buffer/**" + - "start-test-validator/**" + - "setup-all/**" + - ".github/workflows/test-write-program-buffer.yaml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: test-write-program-buffer-${{ github.ref }} + cancel-in-progress: true + +jobs: + integration: + name: ${{ matrix.scenario }} (solana ${{ matrix.solana-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + solana-version: ["4.1.2"] + scenario: [fresh, delta, clamp, authority, nearmax] + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - uses: ./setup-all + with: + solana_version: ${{ matrix.solana-version }} + + - name: Start local validator + id: validator + uses: ./start-test-validator + + - name: Prepare scenario + id: prepare + shell: bash + env: + RPC_URL: ${{ steps.validator.outputs.rpc-url }} + run: ./write-program-buffer/tests/integration/prepare-${{ matrix.scenario }}.sh + + - name: Run write-program-buffer action + id: run + uses: ./write-program-buffer + with: + program-id: ${{ steps.prepare.outputs.program-id }} + program: fixture-${{ matrix.scenario }} + rpc-url: ${{ steps.validator.outputs.rpc-url }} + keypair: ${{ steps.prepare.outputs.keypair }} + buffer-authority-address: ${{ steps.prepare.outputs.buffer-authority }} + + - name: Assert scenario + shell: bash + env: + RPC_URL: ${{ steps.validator.outputs.rpc-url }} + BUFFER: ${{ steps.run.outputs.buffer }} + PROGRAM_ID: ${{ steps.prepare.outputs.program-id }} + DEPLOYER: ${{ steps.prepare.outputs.deployer }} + BUFFER_AUTHORITY: ${{ steps.prepare.outputs.buffer-authority }} + PRE_LEN: ${{ steps.prepare.outputs.pre-len }} + run: ./write-program-buffer/tests/integration/assert-${{ matrix.scenario }}.sh + + - name: Dump validator logs on failure + if: failure() + shell: bash + run: | + tail -100 "$RUNNER_TEMP/validator-stdout.log" || true + tail -100 "${{ steps.validator.outputs.ledger-dir }}/validator.log" || true diff --git a/start-test-validator/action.yaml b/start-test-validator/action.yaml new file mode 100644 index 0000000..4480ff5 --- /dev/null +++ b/start-test-validator/action.yaml @@ -0,0 +1,57 @@ +name: "Start Test Validator" +description: "Starts a local solana-test-validator in the background and waits until it is healthy" + +inputs: + ledger-dir: + description: "Ledger directory (defaults to /test-ledger)" + required: false + default: "" + max-attempts: + description: "Maximum health check attempts, roughly one per second" + required: false + default: "60" + +outputs: + rpc-url: + description: "RPC URL of the started validator" + value: ${{ steps.start.outputs.rpc-url }} + ledger-dir: + description: "Ledger directory of the started validator" + value: ${{ steps.start.outputs.ledger-dir }} + +runs: + using: "composite" + steps: + - name: Start validator + id: start + shell: bash + env: + LEDGER_DIR_INPUT: ${{ inputs.ledger-dir }} + run: | + RPC_URL="http://127.0.0.1:8899" + LEDGER_DIR="$LEDGER_DIR_INPUT" + if [ -z "$LEDGER_DIR" ]; then + LEDGER_DIR="${RUNNER_TEMP:-/tmp}/test-ledger" + fi + + rm -rf "$LEDGER_DIR" + solana-test-validator --reset --quiet --ledger "$LEDGER_DIR" > "${RUNNER_TEMP:-/tmp}/validator-stdout.log" 2>&1 & + echo "Validator started with PID $!" + + echo "rpc-url=$RPC_URL" >> "$GITHUB_OUTPUT" + echo "ledger-dir=$LEDGER_DIR" >> "$GITHUB_OUTPUT" + + - name: Wait for validator health + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 + with: + timeout_seconds: 5 + max_attempts: ${{ inputs.max-attempts }} + retry_wait_seconds: 1 + command: solana cluster-version -u ${{ steps.start.outputs.rpc-url }} + + - name: Dump validator logs if unhealthy + if: failure() + shell: bash + run: | + tail -50 "${RUNNER_TEMP:-/tmp}/validator-stdout.log" 2>/dev/null || true + tail -50 "${{ steps.start.outputs.ledger-dir }}/validator.log" 2>/dev/null || true diff --git a/write-program-buffer/tests/fixtures/program-big.so b/write-program-buffer/tests/fixtures/program-big.so new file mode 100755 index 0000000..ce6700a Binary files /dev/null and b/write-program-buffer/tests/fixtures/program-big.so differ diff --git a/write-program-buffer/tests/fixtures/program-huge.so.gz b/write-program-buffer/tests/fixtures/program-huge.so.gz new file mode 100644 index 0000000..3cc42d8 Binary files /dev/null and b/write-program-buffer/tests/fixtures/program-huge.so.gz differ diff --git a/write-program-buffer/tests/fixtures/program-medium.so b/write-program-buffer/tests/fixtures/program-medium.so new file mode 100755 index 0000000..b8563e7 Binary files /dev/null and b/write-program-buffer/tests/fixtures/program-medium.so differ diff --git a/write-program-buffer/tests/fixtures/program-small.so b/write-program-buffer/tests/fixtures/program-small.so new file mode 100755 index 0000000..96a6391 Binary files /dev/null and b/write-program-buffer/tests/fixtures/program-small.so differ diff --git a/write-program-buffer/tests/fixtures/program/Cargo.lock b/write-program-buffer/tests/fixtures/program/Cargo.lock new file mode 100644 index 0000000..ca7509e --- /dev/null +++ b/write-program-buffer/tests/fixtures/program/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "fixture-program" +version = "0.1.0" diff --git a/write-program-buffer/tests/fixtures/program/Cargo.toml b/write-program-buffer/tests/fixtures/program/Cargo.toml new file mode 100644 index 0000000..aa1e1bf --- /dev/null +++ b/write-program-buffer/tests/fixtures/program/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "fixture-program" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[features] +medium = [] +big = [] +huge = [] + +[workspace] diff --git a/write-program-buffer/tests/fixtures/program/src/lib.rs b/write-program-buffer/tests/fixtures/program/src/lib.rs new file mode 100644 index 0000000..27c9046 --- /dev/null +++ b/write-program-buffer/tests/fixtures/program/src/lib.rs @@ -0,0 +1,25 @@ +#![no_std] + +#[cfg(feature = "medium")] +#[no_mangle] +pub static PAD: [u8; 20_000] = [1; 20_000]; + +#[cfg(feature = "big")] +#[no_mangle] +pub static PAD: [u8; 4096] = [1; 4096]; + +#[cfg(feature = "huge")] +#[no_mangle] +pub static PAD: [u8; 10_481_816] = [1; 10_481_816]; + +#[no_mangle] +pub extern "C" fn entrypoint(_input: *mut u8) -> u64 { + #[cfg(any(feature = "medium", feature = "big", feature = "huge"))] + core::hint::black_box(&PAD); + 0 +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/write-program-buffer/tests/fixtures/rebuild.sh b/write-program-buffer/tests/fixtures/rebuild.sh new file mode 100755 index 0000000..995cce5 --- /dev/null +++ b/write-program-buffer/tests/fixtures/rebuild.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" + +command -v cargo-build-sbf >/dev/null || { + echo "cargo-build-sbf is required, install the agave tool suite first" >&2 + exit 1 +} + +build() { + local variant="$1" feature="$2" + local args=(--arch v3 --manifest-path program/Cargo.toml --sbf-out-dir program/out) + if [ -n "$feature" ]; then + args+=(--features "$feature") + fi + cargo-build-sbf "${args[@]}" + cp program/out/fixture_program.so "program-$variant.so" + rm -rf program/out program/target +} + +build small "" +build medium medium +build big big +build huge huge + +SMALL=$(wc -c < program-small.so | tr -d ' ') +MEDIUM=$(wc -c < program-medium.so | tr -d ' ') +BIG=$(wc -c < program-big.so | tr -d ' ') +HUGE=$(wc -c < program-huge.so | tr -d ' ') +echo "small=$SMALL medium=$MEDIUM big=$BIG huge=$HUGE" + +if [ "$BIG" -le "$SMALL" ] || [ $((BIG - SMALL)) -ge 10240 ]; then + echo "Size band violated: big-small delta must be in (0, 10240)" >&2 + exit 1 +fi +if [ $((MEDIUM - SMALL)) -le 10240 ]; then + echo "Size band violated: medium-small delta must exceed 10240" >&2 + exit 1 +fi +if [ "$HUGE" -le 10477475 ] || [ "$HUGE" -gt 10485715 ]; then + echo "Size band violated: huge must be in (10477475, 10485715]" >&2 + exit 1 +fi + +gzip -9 -n -c program-huge.so > program-huge.so.gz +rm program-huge.so +echo "Fixtures rebuilt" diff --git a/write-program-buffer/tests/integration/assert-authority.sh b/write-program-buffer/tests/integration/assert-authority.sh new file mode 100755 index 0000000..9ac05da --- /dev/null +++ b/write-program-buffer/tests/integration/assert-authority.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +RPC_URL="${RPC_URL:-http://127.0.0.1:8899}" +ARTIFACT="target/deploy/fixture-authority.so" + +fail() { + echo "ASSERT FAIL: $1" >&2 + exit 1 +} + +[ -n "${BUFFER:-}" ] || fail "action did not output a buffer address" +[ -n "${BUFFER_AUTHORITY:-}" ] || fail "BUFFER_AUTHORITY env not set" +[ -n "${DEPLOYER:-}" ] || fail "DEPLOYER env not set" + +BUFFER_INFO=$(solana program show "$BUFFER" -u "$RPC_URL") +echo "$BUFFER_INFO" +AUTHORITY=$(echo "$BUFFER_INFO" | grep "Authority:" | awk '{print $2}') +[ "$AUTHORITY" = "$BUFFER_AUTHORITY" ] || fail "buffer authority is $AUTHORITY, expected $BUFFER_AUTHORITY" +[ "$AUTHORITY" != "$DEPLOYER" ] || fail "buffer authority still equals the deployer" + +DUMP="$(mktemp)" +solana program dump "$BUFFER" "$DUMP" -u "$RPC_URL" || fail "could not dump buffer $BUFFER" +cmp -s "$ARTIFACT" "$DUMP" || fail "buffer contents differ from artifact" +rm -f "$DUMP" + +echo "Authority transfer assertions passed" diff --git a/write-program-buffer/tests/integration/assert-clamp.sh b/write-program-buffer/tests/integration/assert-clamp.sh new file mode 100755 index 0000000..44bb03f --- /dev/null +++ b/write-program-buffer/tests/integration/assert-clamp.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +RPC_URL="${RPC_URL:-http://127.0.0.1:8899}" +ARTIFACT="target/deploy/fixture-clamp.so" +MIN_EXTEND_SIZE=10240 + +fail() { + echo "ASSERT FAIL: $1" >&2 + exit 1 +} + +[ -n "${BUFFER:-}" ] || fail "action did not output a buffer address" +[ -n "${PROGRAM_ID:-}" ] || fail "PROGRAM_ID env not set" +[ -n "${PRE_LEN:-}" ] || fail "PRE_LEN env not set" + +POST_LEN=$(solana program show "$PROGRAM_ID" -u "$RPC_URL" | grep "Data Length:" | sed -E 's/.*Data Length: ([0-9]+).*/\1/' | cut -d ' ' -f1) +GROWTH=$((POST_LEN - PRE_LEN)) +[ "$GROWTH" -eq "$MIN_EXTEND_SIZE" ] || fail "program grew by $GROWTH bytes, expected exactly $MIN_EXTEND_SIZE" + +DUMP="$(mktemp)" +solana program dump "$BUFFER" "$DUMP" -u "$RPC_URL" || fail "could not dump buffer $BUFFER" +cmp -s "$ARTIFACT" "$DUMP" || fail "buffer contents differ from artifact" +rm -f "$DUMP" + +echo "Clamp regression assertions passed: program grew by exactly $MIN_EXTEND_SIZE bytes" diff --git a/write-program-buffer/tests/integration/assert-delta.sh b/write-program-buffer/tests/integration/assert-delta.sh new file mode 100755 index 0000000..8ce2731 --- /dev/null +++ b/write-program-buffer/tests/integration/assert-delta.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +RPC_URL="${RPC_URL:-http://127.0.0.1:8899}" +ARTIFACT="target/deploy/fixture-delta.so" +MIN_EXTEND_SIZE=10240 + +fail() { + echo "ASSERT FAIL: $1" >&2 + exit 1 +} + +[ -n "${BUFFER:-}" ] || fail "action did not output a buffer address" +[ -n "${PROGRAM_ID:-}" ] || fail "PROGRAM_ID env not set" +[ -n "${PRE_LEN:-}" ] || fail "PRE_LEN env not set" + +REQUIRED_SIZE=$(wc -c < "$ARTIFACT" | tr -d ' ') +EXPECTED_GROWTH=$((REQUIRED_SIZE - PRE_LEN)) +[ "$EXPECTED_GROWTH" -gt "$MIN_EXTEND_SIZE" ] || fail "scenario setup invalid: expected growth $EXPECTED_GROWTH must exceed $MIN_EXTEND_SIZE" + +POST_LEN=$(solana program show "$PROGRAM_ID" -u "$RPC_URL" | grep "Data Length:" | sed -E 's/.*Data Length: ([0-9]+).*/\1/' | cut -d ' ' -f1) +GROWTH=$((POST_LEN - PRE_LEN)) +[ "$GROWTH" -eq "$EXPECTED_GROWTH" ] || fail "program grew by $GROWTH bytes, expected the exact delta $EXPECTED_GROWTH" + +DUMP="$(mktemp)" +solana program dump "$BUFFER" "$DUMP" -u "$RPC_URL" || fail "could not dump buffer $BUFFER" +cmp -s "$ARTIFACT" "$DUMP" || fail "buffer contents differ from artifact" +rm -f "$DUMP" + +echo "Plain-delta extend assertions passed: program grew by exactly $EXPECTED_GROWTH bytes" diff --git a/write-program-buffer/tests/integration/assert-fresh.sh b/write-program-buffer/tests/integration/assert-fresh.sh new file mode 100755 index 0000000..31d3e88 --- /dev/null +++ b/write-program-buffer/tests/integration/assert-fresh.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +RPC_URL="${RPC_URL:-http://127.0.0.1:8899}" +ARTIFACT="target/deploy/fixture-fresh.so" + +fail() { + echo "ASSERT FAIL: $1" >&2 + exit 1 +} + +[ -n "${BUFFER:-}" ] || fail "action did not output a buffer address" +[ -n "${PROGRAM_ID:-}" ] || fail "PROGRAM_ID env not set" +[ -n "${DEPLOYER:-}" ] || fail "DEPLOYER env not set" + +DUMP="$(mktemp)" +solana program dump "$BUFFER" "$DUMP" -u "$RPC_URL" || fail "could not dump buffer $BUFFER" +cmp -s "$ARTIFACT" "$DUMP" || fail "buffer contents differ from artifact" +rm -f "$DUMP" + +BUFFER_INFO=$(solana program show "$BUFFER" -u "$RPC_URL") +echo "$BUFFER_INFO" +AUTHORITY=$(echo "$BUFFER_INFO" | grep "Authority:" | awk '{print $2}') +[ "$AUTHORITY" = "$DEPLOYER" ] || fail "buffer authority is $AUTHORITY, expected deployer $DEPLOYER" + +ABSENCE_CHECK=$(solana program show "$PROGRAM_ID" -u "$RPC_URL" 2>&1 || true) +if ! echo "$ABSENCE_CHECK" | grep -q "Unable to find the account"; then + fail "program $PROGRAM_ID unexpectedly exists or the absence check errored: $ABSENCE_CHECK" +fi + +echo "Fresh-program scenario assertions passed" diff --git a/write-program-buffer/tests/integration/assert-nearmax.sh b/write-program-buffer/tests/integration/assert-nearmax.sh new file mode 100755 index 0000000..6138c73 --- /dev/null +++ b/write-program-buffer/tests/integration/assert-nearmax.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +RPC_URL="${RPC_URL:-http://127.0.0.1:8899}" +ARTIFACT="target/deploy/fixture-nearmax.so" +MAX_PERMITTED_DATA_LENGTH=10485760 +PROGRAMDATA_METADATA_SIZE=45 +MAX_PROGRAM_SIZE=$((MAX_PERMITTED_DATA_LENGTH - PROGRAMDATA_METADATA_SIZE)) + +fail() { + echo "ASSERT FAIL: $1" >&2 + exit 1 +} + +[ -n "${BUFFER:-}" ] || fail "action did not output a buffer address" +[ -n "${PROGRAM_ID:-}" ] || fail "PROGRAM_ID env not set" +[ -n "${PRE_LEN:-}" ] || fail "PRE_LEN env not set" + +POST_LEN=$(solana program show "$PROGRAM_ID" -u "$RPC_URL" | grep "Data Length:" | sed -E 's/.*Data Length: ([0-9]+).*/\1/' | cut -d ' ' -f1) +[ "$POST_LEN" -eq "$MAX_PROGRAM_SIZE" ] || fail "program data length is $POST_LEN, expected the exact maximum $MAX_PROGRAM_SIZE" +echo "Program extended from $PRE_LEN to $POST_LEN (exact headroom of $((POST_LEN - PRE_LEN)) bytes)" + +DUMP="$(mktemp)" +solana program dump "$BUFFER" "$DUMP" -u "$RPC_URL" || fail "could not dump buffer $BUFFER" +cmp -s "$ARTIFACT" "$DUMP" || fail "buffer contents differ from artifact" +rm -f "$DUMP" + +echo "Near-max extend assertions passed" diff --git a/write-program-buffer/tests/integration/prepare-authority.sh b/write-program-buffer/tests/integration/prepare-authority.sh new file mode 100755 index 0000000..d840aae --- /dev/null +++ b/write-program-buffer/tests/integration/prepare-authority.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +RPC_URL="${RPC_URL:-http://127.0.0.1:8899}" +FIXTURES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../fixtures" && pwd)" +SCENARIO_DIR="${RUNNER_TEMP:-/tmp}/scenario-authority" + +mkdir -p "$SCENARIO_DIR" target/deploy + +solana-keygen new -s --no-bip39-passphrase --force -o "$SCENARIO_DIR/deployer.json" >/dev/null +solana-keygen new -s --no-bip39-passphrase --force -o "$SCENARIO_DIR/program-id.json" >/dev/null +solana-keygen new -s --no-bip39-passphrase --force -o "$SCENARIO_DIR/authority-target.json" >/dev/null +DEPLOYER=$(solana-keygen pubkey "$SCENARIO_DIR/deployer.json") +PROGRAM_ID=$(solana-keygen pubkey "$SCENARIO_DIR/program-id.json") +AUTHORITY_TARGET=$(solana-keygen pubkey "$SCENARIO_DIR/authority-target.json") + +for i in 1 2 3; do + solana airdrop 100 "$DEPLOYER" -u "$RPC_URL" && break + if [ "$i" -eq 3 ]; then + echo "Airdrop failed after 3 attempts" >&2 + exit 1 + fi + sleep 2 +done + +solana program deploy "$FIXTURES_DIR/program-small.so" \ + --program-id "$SCENARIO_DIR/program-id.json" \ + -u "$RPC_URL" -k "$SCENARIO_DIR/deployer.json" \ + --commitment confirmed + +cp "$FIXTURES_DIR/program-small.so" target/deploy/fixture-authority.so + +echo "Prepared authority scenario: deployer=$DEPLOYER program-id=$PROGRAM_ID authority-target=$AUTHORITY_TARGET" +{ + echo "keypair=$(cat "$SCENARIO_DIR/deployer.json")" + echo "deployer=$DEPLOYER" + echo "program-id=$PROGRAM_ID" + echo "buffer-authority=$AUTHORITY_TARGET" +} >> "$GITHUB_OUTPUT" diff --git a/write-program-buffer/tests/integration/prepare-clamp.sh b/write-program-buffer/tests/integration/prepare-clamp.sh new file mode 100755 index 0000000..d5f81dc --- /dev/null +++ b/write-program-buffer/tests/integration/prepare-clamp.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +RPC_URL="${RPC_URL:-http://127.0.0.1:8899}" +FIXTURES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../fixtures" && pwd)" +SCENARIO_DIR="${RUNNER_TEMP:-/tmp}/scenario-clamp" + +SMALL_SIZE=$(wc -c < "$FIXTURES_DIR/program-small.so" | tr -d ' ') +BIG_SIZE=$(wc -c < "$FIXTURES_DIR/program-big.so" | tr -d ' ') +DELTA=$((BIG_SIZE - SMALL_SIZE)) +if [ "$DELTA" -le 0 ] || [ "$DELTA" -ge 10240 ]; then + echo "Fixture drift: big-small delta of $DELTA bytes is outside (0, 10240)" >&2 + exit 1 +fi + +mkdir -p "$SCENARIO_DIR" target/deploy + +solana-keygen new -s --no-bip39-passphrase --force -o "$SCENARIO_DIR/deployer.json" >/dev/null +solana-keygen new -s --no-bip39-passphrase --force -o "$SCENARIO_DIR/program-id.json" >/dev/null +DEPLOYER=$(solana-keygen pubkey "$SCENARIO_DIR/deployer.json") +PROGRAM_ID=$(solana-keygen pubkey "$SCENARIO_DIR/program-id.json") + +for i in 1 2 3; do + solana airdrop 100 "$DEPLOYER" -u "$RPC_URL" && break + if [ "$i" -eq 3 ]; then + echo "Airdrop failed after 3 attempts" >&2 + exit 1 + fi + sleep 2 +done + +solana program deploy "$FIXTURES_DIR/program-small.so" \ + --program-id "$SCENARIO_DIR/program-id.json" \ + -u "$RPC_URL" -k "$SCENARIO_DIR/deployer.json" \ + --commitment confirmed + +PRE_LEN=$(solana program show "$PROGRAM_ID" -u "$RPC_URL" | grep "Data Length:" | sed -E 's/.*Data Length: ([0-9]+).*/\1/' | cut -d ' ' -f1) +if [ -z "$PRE_LEN" ]; then + echo "Could not read deployed program size" >&2 + exit 1 +fi + +cp "$FIXTURES_DIR/program-big.so" target/deploy/fixture-clamp.so + +echo "Prepared clamp scenario: deployer=$DEPLOYER program-id=$PROGRAM_ID pre-len=$PRE_LEN" +{ + echo "keypair=$(cat "$SCENARIO_DIR/deployer.json")" + echo "deployer=$DEPLOYER" + echo "program-id=$PROGRAM_ID" + echo "buffer-authority=$DEPLOYER" + echo "pre-len=$PRE_LEN" +} >> "$GITHUB_OUTPUT" diff --git a/write-program-buffer/tests/integration/prepare-delta.sh b/write-program-buffer/tests/integration/prepare-delta.sh new file mode 100755 index 0000000..444e9af --- /dev/null +++ b/write-program-buffer/tests/integration/prepare-delta.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -euo pipefail + +RPC_URL="${RPC_URL:-http://127.0.0.1:8899}" +FIXTURES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../fixtures" && pwd)" +SCENARIO_DIR="${RUNNER_TEMP:-/tmp}/scenario-delta" +MIN_EXTEND_SIZE=10240 + +SMALL_SIZE=$(wc -c < "$FIXTURES_DIR/program-small.so" | tr -d ' ') +MEDIUM_SIZE=$(wc -c < "$FIXTURES_DIR/program-medium.so" | tr -d ' ') +DELTA=$((MEDIUM_SIZE - SMALL_SIZE)) +if [ "$DELTA" -le "$MIN_EXTEND_SIZE" ]; then + echo "Fixture drift: medium-small delta of $DELTA bytes must exceed $MIN_EXTEND_SIZE" >&2 + exit 1 +fi + +mkdir -p "$SCENARIO_DIR" target/deploy + +solana-keygen new -s --no-bip39-passphrase --force -o "$SCENARIO_DIR/deployer.json" >/dev/null +solana-keygen new -s --no-bip39-passphrase --force -o "$SCENARIO_DIR/program-id.json" >/dev/null +DEPLOYER=$(solana-keygen pubkey "$SCENARIO_DIR/deployer.json") +PROGRAM_ID=$(solana-keygen pubkey "$SCENARIO_DIR/program-id.json") + +for i in 1 2 3; do + solana airdrop 100 "$DEPLOYER" -u "$RPC_URL" && break + if [ "$i" -eq 3 ]; then + echo "Airdrop failed after 3 attempts" >&2 + exit 1 + fi + sleep 2 +done + +solana program deploy "$FIXTURES_DIR/program-small.so" \ + --program-id "$SCENARIO_DIR/program-id.json" \ + -u "$RPC_URL" -k "$SCENARIO_DIR/deployer.json" \ + --commitment confirmed + +PRE_LEN=$(solana program show "$PROGRAM_ID" -u "$RPC_URL" | grep "Data Length:" | sed -E 's/.*Data Length: ([0-9]+).*/\1/' | cut -d ' ' -f1) +if [ -z "$PRE_LEN" ]; then + echo "Could not read deployed program size" >&2 + exit 1 +fi + +cp "$FIXTURES_DIR/program-medium.so" target/deploy/fixture-delta.so + +echo "Prepared delta scenario: deployer=$DEPLOYER program-id=$PROGRAM_ID pre-len=$PRE_LEN delta=$DELTA" +{ + echo "keypair=$(cat "$SCENARIO_DIR/deployer.json")" + echo "deployer=$DEPLOYER" + echo "program-id=$PROGRAM_ID" + echo "buffer-authority=$DEPLOYER" + echo "pre-len=$PRE_LEN" +} >> "$GITHUB_OUTPUT" diff --git a/write-program-buffer/tests/integration/prepare-fresh.sh b/write-program-buffer/tests/integration/prepare-fresh.sh new file mode 100755 index 0000000..9113b04 --- /dev/null +++ b/write-program-buffer/tests/integration/prepare-fresh.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +RPC_URL="${RPC_URL:-http://127.0.0.1:8899}" +FIXTURES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../fixtures" && pwd)" +SCENARIO_DIR="${RUNNER_TEMP:-/tmp}/scenario-fresh" + +mkdir -p "$SCENARIO_DIR" target/deploy + +solana-keygen new -s --no-bip39-passphrase --force -o "$SCENARIO_DIR/deployer.json" >/dev/null +solana-keygen new -s --no-bip39-passphrase --force -o "$SCENARIO_DIR/program-id.json" >/dev/null +DEPLOYER=$(solana-keygen pubkey "$SCENARIO_DIR/deployer.json") +PROGRAM_ID=$(solana-keygen pubkey "$SCENARIO_DIR/program-id.json") + +for i in 1 2 3; do + solana airdrop 100 "$DEPLOYER" -u "$RPC_URL" && break + if [ "$i" -eq 3 ]; then + echo "Airdrop failed after 3 attempts" >&2 + exit 1 + fi + sleep 2 +done + +cp "$FIXTURES_DIR/program-small.so" target/deploy/fixture-fresh.so + +echo "Prepared fresh scenario: deployer=$DEPLOYER program-id=$PROGRAM_ID" +{ + echo "keypair=$(cat "$SCENARIO_DIR/deployer.json")" + echo "deployer=$DEPLOYER" + echo "program-id=$PROGRAM_ID" + echo "buffer-authority=$DEPLOYER" +} >> "$GITHUB_OUTPUT" diff --git a/write-program-buffer/tests/integration/prepare-nearmax.sh b/write-program-buffer/tests/integration/prepare-nearmax.sh new file mode 100755 index 0000000..232d55b --- /dev/null +++ b/write-program-buffer/tests/integration/prepare-nearmax.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -euo pipefail + +RPC_URL="${RPC_URL:-http://127.0.0.1:8899}" +FIXTURES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../fixtures" && pwd)" +SCENARIO_DIR="${RUNNER_TEMP:-/tmp}/scenario-nearmax" +MAX_PERMITTED_DATA_LENGTH=10485760 +PROGRAMDATA_METADATA_SIZE=45 +MAX_PROGRAM_SIZE=$((MAX_PERMITTED_DATA_LENGTH - PROGRAMDATA_METADATA_SIZE)) +MIN_EXTEND_SIZE=10240 +ARTIFACT_MARGIN=2000 + +mkdir -p "$SCENARIO_DIR" target/deploy + +gunzip -c "$FIXTURES_DIR/program-huge.so.gz" > target/deploy/fixture-nearmax.so +HUGE_SIZE=$(wc -c < target/deploy/fixture-nearmax.so | tr -d ' ') +TARGET_CURRENT=$((HUGE_SIZE - ARTIFACT_MARGIN)) +HEADROOM=$((MAX_PROGRAM_SIZE - TARGET_CURRENT)) + +if [ "$HUGE_SIZE" -gt "$MAX_PROGRAM_SIZE" ]; then + echo "Fixture drift: huge artifact of $HUGE_SIZE bytes exceeds max program size $MAX_PROGRAM_SIZE" >&2 + exit 1 +fi +if [ "$HEADROOM" -le 0 ] || [ "$HEADROOM" -ge "$MIN_EXTEND_SIZE" ]; then + echo "Fixture drift: pre-extend headroom of $HEADROOM bytes is outside (0, $MIN_EXTEND_SIZE)" >&2 + exit 1 +fi + +solana-keygen new -s --no-bip39-passphrase --force -o "$SCENARIO_DIR/deployer.json" >/dev/null +solana-keygen new -s --no-bip39-passphrase --force -o "$SCENARIO_DIR/program-id.json" >/dev/null +DEPLOYER=$(solana-keygen pubkey "$SCENARIO_DIR/deployer.json") +PROGRAM_ID=$(solana-keygen pubkey "$SCENARIO_DIR/program-id.json") + +for i in 1 2 3; do + solana airdrop 200 "$DEPLOYER" -u "$RPC_URL" && break + if [ "$i" -eq 3 ]; then + echo "Airdrop failed after 3 attempts" >&2 + exit 1 + fi + sleep 2 +done + +solana program deploy "$FIXTURES_DIR/program-small.so" \ + --program-id "$SCENARIO_DIR/program-id.json" \ + -u "$RPC_URL" -k "$SCENARIO_DIR/deployer.json" \ + --commitment confirmed + +read_data_len() { + solana program show "$PROGRAM_ID" -u "$RPC_URL" | grep "Data Length:" | sed -E 's/.*Data Length: ([0-9]+).*/\1/' | cut -d ' ' -f1 +} + +for i in 1 2 3; do + CURRENT_LEN=$(read_data_len) + REMAINING=$((TARGET_CURRENT - CURRENT_LEN)) + if [ "$REMAINING" -le 0 ]; then + break + fi + solana program extend "$PROGRAM_ID" "$REMAINING" \ + -u "$RPC_URL" -k "$SCENARIO_DIR/deployer.json" \ + --commitment confirmed && continue + if [ "$i" -eq 3 ]; then + echo "Setup extend failed after 3 attempts" >&2 + exit 1 + fi + sleep 2 +done + +PRE_LEN=$(read_data_len) +if [ "$PRE_LEN" -ne "$TARGET_CURRENT" ]; then + echo "Setup failed: program data length is $PRE_LEN, expected $TARGET_CURRENT" >&2 + exit 1 +fi + +echo "Prepared nearmax scenario: deployer=$DEPLOYER program-id=$PROGRAM_ID pre-len=$PRE_LEN headroom=$HEADROOM" +{ + echo "keypair=$(cat "$SCENARIO_DIR/deployer.json")" + echo "deployer=$DEPLOYER" + echo "program-id=$PROGRAM_ID" + echo "buffer-authority=$DEPLOYER" + echo "pre-len=$PRE_LEN" +} >> "$GITHUB_OUTPUT"