diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index f15409f9..e6e6fdf2 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -155,74 +155,68 @@ jobs: echo "${ZOMBIENET_BIN_DIR}" >> "$GITHUB_PATH" echo "ZOMBIENET_BINARY=zombienet-linux-x64" >> "$GITHUB_ENV" - # Westend parachain - - name: Run authorize and store (PAPI, RPC node, Westend parachain) + # Westend parachain tests + - name: Start services (Westend parachain) working-directory: examples run: | export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test" echo "TEST_DIR=$TEST_DIR" >> "$GITHUB_ENV" mkdir -p "$TEST_DIR" - just run-authorize-and-store "bulletin-westend-runtime" "ws" - - name: Run authorize and store (PAPI, smoldot, Westend parachain) + just start-services "bulletin-westend-runtime" + - name: Test authorize and store (PAPI, RPC node, Westend parachain) working-directory: examples - run: | - export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test" - echo "TEST_DIR=$TEST_DIR" >> "$GITHUB_ENV" - mkdir -p "$TEST_DIR" - just run-authorize-and-store "bulletin-westend-runtime" "smoldot" - - name: Run store chunked data + DAG-PB (PJS-API, RPC node, Westend parachain) + run: just test-authorize-and-store "bulletin-westend-runtime" "ws" + - name: Test authorize and store (PAPI, smoldot, Westend parachain) working-directory: examples - run: | - export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test" - echo "TEST_DIR=$TEST_DIR" >> "$GITHUB_ENV" - mkdir -p "$TEST_DIR" - just run-store-chunked-data "bulletin-westend-runtime" + run: just test-authorize-and-store "bulletin-westend-runtime" "smoldot" + - name: Test store chunked data + DAG-PB (PJS-API, RPC node, Westend parachain) + working-directory: examples + run: just test-store-chunked-data "bulletin-westend-runtime" - name: Run store big data (PJS-API, RPC node, Westend parachain) working-directory: examples - run: | - export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test" - echo "TEST_DIR=$TEST_DIR" >> $GITHUB_ENV - mkdir -p "$TEST_DIR" - just run-store-big-data "bulletin-westend-runtime" + run: just run-store-big-data "bulletin-westend-runtime" + - name: Stop services (Westend parachain) + if: always() + working-directory: examples + run: just stop-services + - name: Upload Westend logs (on failure) + if: failure() + uses: actions/upload-artifact@v4 + with: + name: westend-failed-logs + path: | + ${{ env.TEST_DIR }}/*.log # TODO: Polkadot parachain - # Polkadot solochain - - name: Run authorize and store (PAPI, RPC node, Polkadot solochain) + # Polkadot solochain tests + - name: Start services (Polkadot solochain) working-directory: examples run: | export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test" echo "TEST_DIR=$TEST_DIR" >> "$GITHUB_ENV" mkdir -p "$TEST_DIR" - just run-authorize-and-store "bulletin-polkadot-runtime" "ws" - - name: Run authorize and store (PAPI, smoldot, Polkadot solochain) + just start-services "bulletin-polkadot-runtime" + - name: Test authorize and store (PAPI, RPC node, Polkadot solochain) working-directory: examples - run: | - export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test" - echo "TEST_DIR=$TEST_DIR" >> "$GITHUB_ENV" - mkdir -p "$TEST_DIR" - just run-authorize-and-store "bulletin-polkadot-runtime" "smoldot" - - name: Run store chunked data + DAG-PB (PJS-API, RPC node, Polkadot solochain) + run: just test-authorize-and-store "bulletin-polkadot-runtime" "ws" + - name: Test authorize and store (PAPI, smoldot, Polkadot solochain) working-directory: examples - run: | - export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test" - echo "TEST_DIR=$TEST_DIR" >> "$GITHUB_ENV" - mkdir -p "$TEST_DIR" - just run-store-chunked-data "bulletin-polkadot-runtime" - + run: just test-authorize-and-store "bulletin-polkadot-runtime" "smoldot" + - name: Test store chunked data + DAG-PB (PJS-API, RPC node, Polkadot solochain) + working-directory: examples + run: just test-store-chunked-data "bulletin-polkadot-runtime" - name: Run store big data (PJS-API, RPC node, Polkadot solochain) working-directory: examples - run: | - export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test" - echo "TEST_DIR=$TEST_DIR" >> $GITHUB_ENV - mkdir -p "$TEST_DIR" - just run-store-big-data "bulletin-polkadot-runtime" - - # Collects logs from the last failed zombienet run. - - name: Upload Zombienet logs (on failure) + run: just run-store-big-data "bulletin-polkadot-runtime" + - name: Stop services (Polkadot solochain) + if: always() + working-directory: examples + run: just stop-services + - name: Upload Polkadot logs (on failure) if: failure() uses: actions/upload-artifact@v4 with: - name: failed-zombienet-logs + name: polkadot-failed-logs path: | ${{ env.TEST_DIR }}/*.log diff --git a/examples/README.md b/examples/README.md index 499a406e..1b6345f3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -9,26 +9,35 @@ Install just with: - `brew install just`, if you're on Mac OS and have `brew` package manager installed, - `sudo apt install just`, if you're using a Linux distribution. -### Run prerequisites - -It's only needed once after checkout or when dependencies change: -- `just build` -- `just npm-install` - -### Run full workflow example -- `just run-authorize-and-store papi` - for PAPI, -- `just run-authorize-and-store pjs` - for PJS. - -#### Run individual commands for manual testing -- `just setup-services papi` - Setup all services (IPFS, zombienet, reconnect, PAPI descriptors), -- `just ipfs-init` - Initialize IPFS (if needed), -- `just ipfs-start` - Start IPFS daemon, -- `just bulletin-solo-zombienet-start` - Start zombienet, -- `just ipfs-connect` - Connect to IPFS nodes, -- `just ipfs-reconnect-start` - Start IPFS reconnect script, -- `just papi-generate` - Generate PAPI descriptors, -- `just run-example papi` - Run example with PAPI or PJS, -- `just teardown-services` - Stop all services +### Quick Start - Run Tests + +Start services once, run tests, then tear down: + +```bash +# 1. Start services for your runtime (only needed once) +just start-services bulletin-westend-runtime +# or +just start-services bulletin-polkadot-runtime + +# 2. Run tests (as many as you want) +just test-authorize-and-store bulletin-westend-runtime ws +just test-authorize-and-store bulletin-westend-runtime smoldot +just test-store-chunked-data bulletin-westend-runtime + +# 3. Stop services when done +just stop-services +``` + +### Available Runtimes + +- `bulletin-westend-runtime` - Westend parachain configuration +- `bulletin-polkadot-runtime` - Polkadot solochain configuration + +### Available Test Commands + +- `test-authorize-and-store ` - Test authorization and storage workflow + - `mode` can be: `ws` (WebSocket RPC) or `smoldot` (light client) +- `test-store-chunked-data ` - Test chunked data storage with DAG-PB ## Manually diff --git a/examples/api.js b/examples/api.js index a5315d3c..5f31669f 100644 --- a/examples/api.js +++ b/examples/api.js @@ -68,12 +68,13 @@ export const TX_MODE_IN_BLOCK = "in-block"; export const TX_MODE_FINALIZED_BLOCK = "finalized-block"; export const TX_MODE_IN_POOL = "in-tx-pool"; -const DEFAULT_TX_TIMEOUT_MS = 60_000; // 60 seconds or 10 blocks +const DEFAULT_TX_TIMEOUT_MS = 120_000; // 120 seconds - includes time for in-block inclusion + stability delay const TX_MODE_CONFIG = { [TX_MODE_IN_BLOCK]: { match: (ev) => ev.type === "txBestBlocksState" && ev.found, log: (txName, ev) => `๐Ÿ“ฆ ${txName} included in block: ${ev.block.hash}`, + stabilityDelayMs: 6000, // Wait 6s for block to stabilize (helps with light client reorgs) }, [TX_MODE_IN_POOL]: { match: (ev) => ev.type === "broadcasted", @@ -113,8 +114,24 @@ function waitForTransaction(tx, signer, txName, txMode = TX_MODE_IN_BLOCK, timeo console.log(`โœ… ${txName} event:`, ev.type); if (!resolved && config.match(ev)) { console.log(config.log(txName, ev)); - cleanup(); - resolve(ev); + + // Mark as resolved immediately to prevent error handler from rejecting + resolved = true; + + // Unsubscribe to stop receiving events + if (sub) sub.unsubscribe(); + + // If config specifies a stability delay, wait before resolving + if (config.stabilityDelayMs) { + console.log(`โณ Waiting ${config.stabilityDelayMs}ms for block stability...`); + setTimeout(() => { + clearTimeout(timeoutId); + resolve(ev); + }, config.stabilityDelayMs); + } else { + clearTimeout(timeoutId); + resolve(ev); + } } }, error: (err) => { diff --git a/examples/authorize_and_store_papi_smoldot.js b/examples/authorize_and_store_papi_smoldot.js index 7b5fabb8..32a4a87f 100644 --- a/examples/authorize_and_store_papi_smoldot.js +++ b/examples/authorize_and_store_papi_smoldot.js @@ -10,7 +10,8 @@ import { cidFromBytes } from "./cid_dag_metadata.js"; import { bulletin } from './.papi/descriptors/dist/index.mjs'; // Constants -const SYNC_WAIT_SEC = 15; +const SYNC_WAIT_SEC = 30; +const OVERALL_TIMEOUT_SEC = 180; // 3 minutes total timeout for the test const SMOLDOT_LOG_LEVEL = 3; // 0=off, 1=error, 2=warn, 3=info, 4=debug, 5=trace const HTTP_IPFS_API = 'http://127.0.0.1:8080' // Local IPFS HTTP gateway @@ -92,7 +93,7 @@ async function createSmoldotClient(chainSpecPath, parachainSpecPath = null) { return { client: createClient(getSmProvider(targetChain)), sd }; } -async function main() { +async function runTestWithTimeout() { await cryptoWaitReady(); // Get chainspec path from command line argument (required - main chain: relay for para, or solo) @@ -118,7 +119,11 @@ async function main() { console.log('๐Ÿ” Checking if chain is ready...'); const bulletinAPI = client.getTypedApi(bulletin); - await waitForChainReady(bulletinAPI); + const isReady = await waitForChainReady(bulletinAPI); + + if (!isReady) { + throw new Error('Chain failed to become ready after maximum retries'); + } // Signers. const { sudoSigner, whoSigner, whoAddress } = setupKeyringAndSigners('//Alice', '//Alice'); @@ -167,4 +172,19 @@ async function main() { } } +async function main() { + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error(`โŒ Test timeout: exceeded ${OVERALL_TIMEOUT_SEC} seconds`)); + }, OVERALL_TIMEOUT_SEC * 1000); + }); + + try { + await Promise.race([runTestWithTimeout(), timeoutPromise]); + } catch (error) { + console.error(error.message || error); + process.exit(1); + } +} + await main(); diff --git a/examples/common.js b/examples/common.js index 159a6107..6d3d1007 100644 --- a/examples/common.js +++ b/examples/common.js @@ -207,12 +207,18 @@ export class NonceManager { * Wait for a PAPI typed API chain to be ready by checking runtime constants. * Retries until the chain is ready or max retries reached. */ -export async function waitForChainReady(typedApi, maxRetries = 10, retryDelayMs = 2000) { +export async function waitForChainReady(typedApi, maxRetries = 20, retryDelayMs = 2000) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { // Check runtime constants to verify chain is accessible const version = typedApi.constants.System.Version; + + // Validate that we got real values, not undefined + if (!version || !version.spec_name || !version.spec_version) { + throw new Error(`Invalid version data: spec_name=${version?.spec_name}, spec_version=${version?.spec_version}`); + } + console.log(`โœ… Chain is ready! Runtime: ${version.spec_name} v${version.spec_version}`); return true; } catch (error) { @@ -220,7 +226,7 @@ export async function waitForChainReady(typedApi, maxRetries = 10, retryDelayMs console.log(`โณ Chain not ready yet (attempt ${attempt}/${maxRetries}), retrying in ${retryDelayMs/1000}s... Error: ${error.message}`); await new Promise(resolve => setTimeout(resolve, retryDelayMs)); } else { - console.log(`โš ๏ธ Chain readiness check failed after ${maxRetries} attempts. Proceeding anyway... Error: ${error.message}`); + console.error(`โŒ Chain readiness check failed after ${maxRetries} attempts (${maxRetries * retryDelayMs / 1000}s total). Error: ${error.message}`); return false; } } diff --git a/examples/justfile b/examples/justfile index 25497fd6..5a06fef4 100644 --- a/examples/justfile +++ b/examples/justfile @@ -2,8 +2,28 @@ # # See README.md for usage instructions. -# Default recipe - run the complete PAPI workflow test -default: (run-authorize-and-store "bulletin-westend-runtime" "ws") +# Default recipe - show available commands +default: + @echo "Polkadot Bulletin Chain - Just Commands" + @echo "" + @echo "Start environment and run tests:" + @echo " just start-services - Start services (westend/polkadot)" + @echo " just test-authorize-and-store - Run auth test (ws/smoldot)" + @echo " just test-store-chunked-data - Run chunked data test" + @echo " just stop-services - Stop all services" + @echo "" + @echo "Available runtimes:" + @echo " bulletin-westend-runtime - Westend parachain" + @echo " bulletin-polkadot-runtime - Polkadot solochain" + @echo "" + @echo "Example workflow:" + @echo " just start-services bulletin-westend-runtime" + @echo " just test-authorize-and-store bulletin-westend-runtime ws" + @echo " just test-store-chunked-data bulletin-westend-runtime" + @echo " just stop-services" + +# Test directory state file for persisting TEST_DIR between recipe calls +TEST_DIR_FILE := justfile_directory() + "/.test-dir" # Setup prerequisites for parachain runtime (polkadot and polkadot-omni-node binaries) # This recipe clones polkadot-sdk, builds required binaries, and copies them to ~/local_bulletin_testing/bin @@ -29,6 +49,22 @@ compute-test-dir: echo "$TEST_DIR"; fi +# Save TEST_DIR to file for use across multiple recipe invocations +save-test-dir test_dir: + #!/usr/bin/env bash + echo "{{ test_dir }}" > {{ TEST_DIR_FILE }} + echo " Saved TEST_DIR to {{ TEST_DIR_FILE }}" + +# Load TEST_DIR from file +load-test-dir: + #!/usr/bin/env bash + if [ ! -f "{{ TEST_DIR_FILE }}" ]; then + echo "โŒ Error: No active test environment found" + echo " Please run 'just start-services ' first" + exit 1 + fi + cat "{{ TEST_DIR_FILE }}" + # Install JavaScript dependencies npm-install: npm install @@ -360,27 +396,62 @@ teardown-services test_dir: just ipfs-shutdown "{{ test_dir }}" echo "โœ… Docker services stopped" -# Run authorize and store example with Docker IPFS +# Start services and save TEST_DIR for subsequent test runs +# Parameters: +# runtime - Runtime name (e.g., "bulletin-polkadot-runtime", "bulletin-westend-runtime") +start-services runtime: npm-install + #!/usr/bin/env bash + set -e + + echo "๐Ÿš€ Starting services for runtime: {{ runtime }}" + + TEST_DIR="$(just compute-test-dir)" + echo " Using TEST_DIR: $TEST_DIR" + + just setup-services "$TEST_DIR" "{{ runtime }}" + just save-test-dir "$TEST_DIR" + + echo "" + echo "โœ… Services started successfully!" + +# Stop services (uses saved TEST_DIR) +stop-services: + #!/usr/bin/env bash + set -e + + echo "๐Ÿ›‘ Stopping services..." + + TEST_DIR="$(just load-test-dir)" + echo " Using TEST_DIR: $TEST_DIR" + + just teardown-services "$TEST_DIR" + + rm -f "{{ TEST_DIR_FILE }}" + echo " Removed {{ TEST_DIR_FILE }}" + + echo "โœ… Services stopped successfully!" + +# Run authorize and store test (without setup/teardown, uses saved TEST_DIR) # Parameters: +# runtime - Runtime name (e.g., "bulletin-polkadot-runtime", "bulletin-westend-runtime") # mode - Connection mode: "ws" (WebSocket RPC node) or "smoldot" (light client) -# runtime - Runtime name (e.g., "bulletin-polkadot-runtime", "bulletin-westend-runtime", "polkadot-bulletin-chain-runtime") -run-authorize-and-store runtime mode="ws": npm-install +test-authorize-and-store runtime mode="ws": #!/usr/bin/env bash set -e if [ "{{ mode }}" = "smoldot" ]; then - echo "๐Ÿš€ Starting authorize and store workflow test (mode: smoldot, runtime: {{ runtime }})..." + echo "๐Ÿงช Running authorize and store test (mode: smoldot, runtime: {{ runtime }})..." SCRIPT_NAME="authorize_and_store_papi_smoldot.js" elif [ "{{ mode }}" = "ws" ]; then - echo "๐Ÿš€ Starting authorize and store workflow test (mode: ws, runtime: {{ runtime }})..." + echo "๐Ÿงช Running authorize and store test (mode: ws, runtime: {{ runtime }})..." SCRIPT_NAME="authorize_and_store_papi.js" else echo "โŒ Error: Invalid mode '{{ mode }}'. Must be 'ws' or 'smoldot'" exit 1 fi - TEST_DIR="$(just compute-test-dir)" - just setup-services "$TEST_DIR" "{{ runtime }}" + TEST_DIR="$(just load-test-dir)" + echo " Using TEST_DIR: $TEST_DIR" set +e # Run the script with chainspec_path parameter only for smoldot mode @@ -403,25 +474,24 @@ run-authorize-and-store runtime mode="ws": npm-install echo "" echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" if [ $EXAMPLE_EXIT -eq 0 ]; then - echo "โœ… Example completed successfully!" + echo "โœ… Test completed successfully!" else - echo "โŒ Example failed with exit code $EXAMPLE_EXIT" + echo "โŒ Test failed with exit code $EXAMPLE_EXIT" fi - just teardown-services "$TEST_DIR" exit $EXAMPLE_EXIT -# Run store chunked data example with Docker IPFS +# Run store chunked data test (without setup/teardown, uses saved TEST_DIR) # Parameters: # runtime - Runtime name (e.g., "bulletin-polkadot-runtime", "bulletin-westend-runtime") -run-store-chunked-data runtime: npm-install +test-store-chunked-data runtime: #!/usr/bin/env bash set -e - echo "๐Ÿš€ Starting store chunked data + DAG-PB workflow test (runtime: {{ runtime }})..." + echo "๐Ÿงช Running store chunked data + DAG-PB test (runtime: {{ runtime }})..." - TEST_DIR="$(just compute-test-dir)" - just setup-services "$TEST_DIR" "{{ runtime }}" + TEST_DIR="$(just load-test-dir)" + echo " Using TEST_DIR: $TEST_DIR" set +e node store_chunked_data.js @@ -430,12 +500,11 @@ run-store-chunked-data runtime: npm-install echo "" echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" if [ $EXAMPLE_EXIT -eq 0 ]; then - echo "โœ… Example completed successfully!" + echo "โœ… Test completed successfully!" else - echo "โŒ Example failed with exit code $EXAMPLE_EXIT" + echo "โŒ Test failed with exit code $EXAMPLE_EXIT" fi - just teardown-services "$TEST_DIR" exit $EXAMPLE_EXIT # Run store big data example. diff --git a/zombienet/bulletin-polkadot-local.toml b/zombienet/bulletin-polkadot-local.toml index 9032173f..760d5a06 100644 --- a/zombienet/bulletin-polkadot-local.toml +++ b/zombienet/bulletin-polkadot-local.toml @@ -11,11 +11,22 @@ name = "alice" p2p_port = 10001 rpc_port = 10000 validator = true -args = ["--ipfs-server", "-lruntime=trace,sub-libp2p::bitswap=trace,runtime::transaction-storage=trace"] +args = [ + "--ipfs-server", + "-lruntime=trace,sub-libp2p::bitswap=trace,runtime::transaction-storage=trace", + # WebSocket P2P on p2p_port + 1 for smoldot light client support + "--listen-addr", + "/ip4/0.0.0.0/tcp/10002/ws", +] [[relaychain.nodes]] name = "bob" p2p_port = 12347 rpc_port = 12346 validator = true -args = ["-lruntime=trace,bitswap=trace,sub-libp2p::bitswap=trace,runtime::transaction-storage=trace"] +args = [ + "-lruntime=trace,bitswap=trace,sub-libp2p::bitswap=trace,runtime::transaction-storage=trace", + # WebSocket P2P on p2p_port + 1 for smoldot light client support + "--listen-addr", + "/ip4/0.0.0.0/tcp/12348/ws", +]