Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
7d53e26
fix: ensure program is extended by minimum size of 10,240 bytes (#1)
sparten11740 Jul 17, 2026
7afbc1a
fix: handle edge cases at max permitted data length
sparten11740 Jul 17, 2026
fc6eaf3
test(write-program-buffer): add program fixtures
sparten11740 Jul 17, 2026
f1263b6
test(write-program-buffer): add validator harness and fresh-program s…
sparten11740 Jul 17, 2026
2ac9ec4
feat(start-test-validator): extract validator startup into reusable a…
sparten11740 Jul 17, 2026
66621fc
refactor(test): run scenarios as a matrix dimension
sparten11740 Jul 17, 2026
a292bf0
test(write-program-buffer): add SIMD-0431 clamp regression scenario
sparten11740 Jul 17, 2026
dc5eea7
test(write-program-buffer): add buffer authority transfer scenario
sparten11740 Jul 17, 2026
2e948be
test(write-program-buffer): add near-max extend scenario
sparten11740 Jul 17, 2026
48fef8f
test(write-program-buffer): drop solana 3.x from the test matrix
sparten11740 Jul 20, 2026
51d7958
chore(start-test-validator): quote GITHUB_OUTPUT redirects
sparten11740 Jul 20, 2026
360dc18
fix(start-test-validator): pass ledger-dir via env and dump startup log
sparten11740 Jul 20, 2026
4ff4790
chore(ci): harden test workflow triggers and permissions
sparten11740 Jul 20, 2026
ef9de81
fix(test): apply review findings to integration scripts
sparten11740 Jul 20, 2026
fe572fb
test(write-program-buffer): add plain-delta extend scenario
sparten11740 Jul 20, 2026
c845566
test(write-program-buffer): add fixture source and rebuild script
sparten11740 Jul 20, 2026
c8dc446
fix(test): capture absence check output before grep to survive pipefail
sparten11740 Jul 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .github/workflows/test-write-program-buffer.yaml
Original file line number Diff line number Diff line change
@@ -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
57 changes: 57 additions & 0 deletions start-test-validator/action.yaml
Original file line number Diff line number Diff line change
@@ -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 <runner temp>/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
19 changes: 19 additions & 0 deletions write-program-buffer/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,32 @@ runs:
if: steps.check-program.outputs.exists == 'true'
shell: bash
run: |
MIN_EXTEND_SIZE=10240
MAX_PERMITTED_DATA_LENGTH=10485760
PROGRAMDATA_METADATA_SIZE=45
MAX_PROGRAM_SIZE=$((MAX_PERMITTED_DATA_LENGTH - PROGRAMDATA_METADATA_SIZE))

REQUIRED_SIZE=$(wc -c < ./target/deploy/${{ inputs.program }}.so)
CURRENT_SIZE="${{ steps.check-program.outputs.data_len }}"
echo "Required size: $REQUIRED_SIZE"
echo "Current size: $CURRENT_SIZE"

if [ "$REQUIRED_SIZE" -gt "$MAX_PROGRAM_SIZE" ]; then
echo "Error: program size of $REQUIRED_SIZE bytes exceeds the maximum program size of $MAX_PROGRAM_SIZE bytes"
exit 1
fi

if [ "$REQUIRED_SIZE" -gt "$CURRENT_SIZE" ]; then
HEADROOM=$((MAX_PROGRAM_SIZE - CURRENT_SIZE))
if [ "$HEADROOM" -lt "$MIN_EXTEND_SIZE" ]; then
echo "Only $HEADROOM bytes of headroom to the maximum account size, the SIMD-0431 minimum does not apply"
MIN_EXTEND_SIZE=$HEADROOM
fi
EXTEND_SIZE=$((REQUIRED_SIZE - CURRENT_SIZE))
if [ "$EXTEND_SIZE" -lt "$MIN_EXTEND_SIZE" ]; then
echo "Extend size of $EXTEND_SIZE bytes is below the SIMD-0431 minimum, extending by $MIN_EXTEND_SIZE bytes instead"
EXTEND_SIZE=$MIN_EXTEND_SIZE
fi
echo "Program needs to be extended by $EXTEND_SIZE bytes"
solana program extend ${{ inputs.program-id }} $EXTEND_SIZE \
--keypair ./deploy-keypair.json \
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added write-program-buffer/tests/fixtures/program-small.so
Binary file not shown.
7 changes: 7 additions & 0 deletions write-program-buffer/tests/fixtures/program/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions write-program-buffer/tests/fixtures/program/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "fixture-program"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[features]
medium = []
big = []
huge = []

[workspace]
25 changes: 25 additions & 0 deletions write-program-buffer/tests/fixtures/program/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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 {}
}
47 changes: 47 additions & 0 deletions write-program-buffer/tests/fixtures/rebuild.sh
Original file line number Diff line number Diff line change
@@ -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"
27 changes: 27 additions & 0 deletions write-program-buffer/tests/integration/assert-authority.sh
Original file line number Diff line number Diff line change
@@ -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"
26 changes: 26 additions & 0 deletions write-program-buffer/tests/integration/assert-clamp.sh
Original file line number Diff line number Diff line change
@@ -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"
30 changes: 30 additions & 0 deletions write-program-buffer/tests/integration/assert-delta.sh
Original file line number Diff line number Diff line change
@@ -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"
31 changes: 31 additions & 0 deletions write-program-buffer/tests/integration/assert-fresh.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading
Loading