diff --git a/README.md b/README.md index ef4dac3b1..cee89a410 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,29 @@ cargo build --release -p polkadot-bulletin-chain POLKADOT_BULLETIN_BINARY_PATH=./target/release/polkadot-bulletin-chain zombienet -p native spawn ./zombienet/bulletin-polkadot-local.toml ``` +### Run Bulletin Westend local chain + +```bash +# 1. Build and create the chain spec +cargo build --release -p bulletin-westend-runtime +./scripts/create_bulletin_westend_spec.sh + +# 2. Spawn the zombienet +POLKADOT_BINARY_PATH=~/local_bridge_testing/bin/polkadot \ +POLKADOT_PARACHAIN_BINARY_PATH=~/local_bridge_testing/bin/polkadot-parachain \ +zombienet -p native spawn ./zombienet/bulletin-westend-local.toml +``` + +#### Assigning Cores (Multi-Core Block Production) + +After the zombienet is running, you need to assign cores to the bulletin parachain for block production. The relay chain is configured with 3 cores (`num_cores = 3`). + +```bash +./scripts/assign_cores.sh ws://localhost:9942 1006 0 1 +``` + +This uses sudo to submit a batch of `coretime.assignCore` extrinsics on the relay chain. + ### Run a production chain (but only with Alice validator) You can override the Alice validator keys here: [adjust\_bp\_spec.sh](./zombienet/adjust_bp_spec.sh) (you should see finalized blocks in the logs). diff --git a/integration-tests/README.md b/integration-tests/README.md new file mode 100644 index 000000000..8c665ab86 --- /dev/null +++ b/integration-tests/README.md @@ -0,0 +1,34 @@ +# Bulletin Chain Integration Tests + +Integration tests for the Polkadot Bulletin Chain (Westend Parachain). + +## Prerequisites + +```bash +mkdir -p ~/local_bridge_testing/bin + +# Build polkadot and polkadot-parachain from polkadot-sdk +# Copy to ~/local_bridge_testing/bin/ + +# Download zombienet +wget "https://github.com/paritytech/zombienet/releases/download/v1.3.133/zombienet-linux-x64" \ + -O ~/local_bridge_testing/bin/zombienet +chmod +x ~/local_bridge_testing/bin/zombienet + +# Generate chain spec +cd polkadot-bulletin-chain +./scripts/create_bulletin_westend_spec.sh + +# Install and start Kubo +wget https://dist.ipfs.tech/kubo/v0.38.1/kubo_v0.38.1_darwin-arm64.tar.gz +tar -xvzf kubo_v0.38.1_darwin-arm64.tar.gz +./kubo/ipfs version +./kubo/ipfs init +./kubo/ipfs daemon & +``` + +## Running Tests + +```bash +./integration-tests/run-test.sh 01-store-data +``` diff --git a/integration-tests/run-test.sh b/integration-tests/run-test.sh new file mode 100755 index 000000000..e440e6729 --- /dev/null +++ b/integration-tests/run-test.sh @@ -0,0 +1,107 @@ +#!/bin/bash +# +# Integration test runner for Polkadot Bulletin Chain (Westend Parachain). +# +# Usage: +# ./run-test.sh +# +# Example: +# ./run-test.sh 01-store-data +# ./run-test.sh 02-elastic-scaling +# +# Prerequisites: +# - polkadot binary at ~/local_bridge_testing/bin/polkadot +# - polkadot-parachain binary at ~/local_bridge_testing/bin/polkadot-parachain +# - zombienet binary at ~/local_bridge_testing/bin/zombienet +# - Node.js installed + +set -e + +# Kill any leftover processes from previous runs (kills all zombienet processes +# maybe too strict? Solution to problems when killing and restarting tests) +cleanup_old_processes() { + echo "Killing any running zombienet processes..." + pkill -9 -f "zombienet.*spawn.*bulletin-westend" 2>/dev/null || true + pkill -9 -f "polkadot.*westend-local" 2>/dev/null || true + pkill -9 -f "polkadot-parachain.*bulletin-westend" 2>/dev/null || true + pkill -9 -f "ipfs-reconnect-westend" 2>/dev/null || true + sleep 1 +} + +# Cleanup function for this test run +cleanup() { + echo "" + echo "Cleaning up test environment..." + + # Kill all child processes + pkill -9 -P $$ 2>/dev/null || true + + # Also kill by pattern in case they escaped the process tree + pkill -9 -f "zombienet.*spawn.*bulletin-westend" 2>/dev/null || true + pkill -9 -f "polkadot.*westend-local" 2>/dev/null || true + pkill -9 -f "polkadot-parachain.*bulletin-westend" 2>/dev/null || true + pkill -9 -f "ipfs-reconnect-westend" 2>/dev/null || true + + echo "Cleanup complete." +} + +# Set trap for cleanup on exit +trap cleanup SIGINT SIGTERM EXIT + +test=$1 + +if [ -z "$test" ]; then + echo "Usage: $0 " + echo "" + echo "Available tests:" + for dir in "${BASH_SOURCE%/*}"/tests/*/; do + if [ -d "$dir" ]; then + basename "$dir" + fi + done + exit 1 +fi + +# Cleanup old processes first +cleanup_old_processes + +# Setup paths +export LOCAL_BRIDGE_TESTING_PATH=${LOCAL_BRIDGE_TESTING_PATH:-~/local_bridge_testing} +export BULLETIN_REPO_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# Binary paths (default paths can be overridden by env vars) +export ZOMBIENET_BINARY=${ZOMBIENET_BINARY:-$LOCAL_BRIDGE_TESTING_PATH/bin/zombienet} +export POLKADOT_BINARY_PATH=${POLKADOT_BINARY_PATH:-$LOCAL_BRIDGE_TESTING_PATH/bin/polkadot} +export POLKADOT_PARACHAIN_BINARY_PATH=${POLKADOT_PARACHAIN_BINARY_PATH:-$LOCAL_BRIDGE_TESTING_PATH/bin/polkadot-parachain} + +# Verify binaries exist +for bin in "$ZOMBIENET_BINARY" "$POLKADOT_BINARY_PATH" "$POLKADOT_PARACHAIN_BINARY_PATH"; do + if [ ! -f "$bin" ]; then + echo "Error: Required binary not found: $bin" + echo "" + echo "Please ensure the following binaries are installed:" + echo " - $ZOMBIENET_BINARY" + echo " - $POLKADOT_BINARY_PATH" + echo " - $POLKADOT_PARACHAIN_BINARY_PATH" + exit 1 + fi +done + +# Create test directory +export TEST_DIR=$(mktemp -d /tmp/bulletin-tests-XXXXX) +mkdir -p "$TEST_DIR/logs" +echo "Test directory: $TEST_DIR" +echo "" + +# Run the test +test_script="${BASH_SOURCE%/*}/tests/$test/run.sh" +if [ ! -f "$test_script" ]; then + echo "Error: Test '$test' not found at $test_script" + exit 1 +fi + +echo "Running test: $test" +echo "================================================" +echo "" + +bash "$test_script" diff --git a/integration-tests/tests/01-store-data/authorize-and-store.js b/integration-tests/tests/01-store-data/authorize-and-store.js new file mode 100644 index 000000000..ef21911a5 --- /dev/null +++ b/integration-tests/tests/01-store-data/authorize-and-store.js @@ -0,0 +1,24 @@ +import { ApiPromise, WsProvider, Keyring } from '@polkadot/api'; +import { cryptoWaitReady, blake2AsU8a } from '@polkadot/util-crypto'; +import { CID } from 'multiformats/cid'; +import { create } from 'multiformats/hashes/digest'; + +const [,, endpoint, seed, data] = process.argv; + +await cryptoWaitReady(); +const api = await ApiPromise.create({ provider: new WsProvider(endpoint), noInitWarn: true }); +const pair = new Keyring({ type: 'sr25519' }).addFromUri(seed); + +// Authorize +console.error('Authorizing...'); +await api.tx.sudo.sudo(api.tx.transactionStorage.authorizeAccount(pair.address, 2, 65536)).signAndSend(pair); +await new Promise(r => setTimeout(r, 7000)); + +// Store +console.error('Storing...'); +const cid = CID.createV1(0x55, create(0xb220, blake2AsU8a(data))); +await api.tx.transactionStorage.store(data).signAndSend(pair); +await new Promise(r => setTimeout(r, 7000)); + +console.log(`OUTPUT_CID=${cid.toString()}`); +await api.disconnect(); diff --git a/integration-tests/tests/01-store-data/check-authorization.js b/integration-tests/tests/01-store-data/check-authorization.js new file mode 100644 index 000000000..c26754975 --- /dev/null +++ b/integration-tests/tests/01-store-data/check-authorization.js @@ -0,0 +1,20 @@ +import { ApiPromise, WsProvider } from '@polkadot/api'; + +const [,, endpoint, account, expected] = process.argv; +const expectAuth = expected === 'true'; + +const api = await ApiPromise.create({ provider: new WsProvider(endpoint), noInitWarn: true }); + +for (let i = 0; i < 10; i++) { + const auth = await api.query.transactionStorage.authorizations({ Account: account }); + if (auth.isSome === expectAuth) { + console.log(expectAuth ? '✅ Authorized' : '✅ Not authorized (expected)'); + await api.disconnect(); + process.exit(0); + } + await new Promise(r => setTimeout(r, 3000)); +} + +console.error(`❌ Authorization mismatch: expected ${expectAuth}`); +await api.disconnect(); +process.exit(1); diff --git a/integration-tests/tests/01-store-data/package.json b/integration-tests/tests/01-store-data/package.json new file mode 100644 index 000000000..14a86c5a7 --- /dev/null +++ b/integration-tests/tests/01-store-data/package.json @@ -0,0 +1,10 @@ +{ + "type": "module", + "dependencies": { + "@polkadot/api": "^16.5.2", + "@polkadot/keyring": "^13.5.8", + "@polkadot/util-crypto": "^13.5.8", + "ipfs-http-client": "^60.0.1", + "multiformats": "^13.4.1" + } +} diff --git a/integration-tests/tests/01-store-data/run.sh b/integration-tests/tests/01-store-data/run.sh new file mode 100755 index 000000000..8048f58f7 --- /dev/null +++ b/integration-tests/tests/01-store-data/run.sh @@ -0,0 +1,56 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BULLETIN_REPO_PATH="${BULLETIN_REPO_PATH:-$(cd "$SCRIPT_DIR/../../.." && pwd)}" + +ZOMBIENET_CONFIG="$BULLETIN_REPO_PATH/zombienet/bulletin-westend-local.toml" +RELAY_ENDPOINT="ws://127.0.0.1:9942" +PARACHAIN_ENDPOINT="ws://127.0.0.1:10000" +IPFS_URL="http://127.0.0.1:5001" +TEST_DATA="0x48656c6c6f20576f726c64" +ALICE="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" + +SPAWNED_PIDS=() +cleanup() { + for pid in "${SPAWNED_PIDS[@]}"; do kill -9 "$pid" 2>/dev/null || true; done + [ -n "$zombienet_pid" ] && pkill -9 -P "$zombienet_pid" 2>/dev/null || true +} +trap cleanup EXIT + +echo "=== Bulletin Westend Data Storage Test ===" + +[ -f "$BULLETIN_REPO_PATH/zombienet/bulletin-westend-spec.json" ] || bash "$BULLETIN_REPO_PATH/scripts/create_bulletin_westend_spec.sh" +[ -d "$SCRIPT_DIR/node_modules" ] || (cd "$SCRIPT_DIR" && npm install) + +echo "Starting network..." +pushd "$BULLETIN_REPO_PATH" > /dev/null +"$ZOMBIENET_BINARY" -p native spawn "$ZOMBIENET_CONFIG" > "$TEST_DIR/logs/zombienet.log" 2>&1 & +zombienet_pid=$! +SPAWNED_PIDS+=($zombienet_pid) +popd > /dev/null + +echo "Waiting for relay chain..." +node "$SCRIPT_DIR/wait-for-chain.js" "$RELAY_ENDPOINT" 2>/dev/null +echo "Waiting for parachain..." +node "$SCRIPT_DIR/wait-for-chain.js" "$PARACHAIN_ENDPOINT" 2>/dev/null + +"$BULLETIN_REPO_PATH/scripts/ipfs-reconnect-westend.sh" > "$TEST_DIR/logs/ipfs.log" 2>&1 & +disown + +echo "Checking no initial authorization..." +node "$SCRIPT_DIR/check-authorization.js" "$PARACHAIN_ENDPOINT" "$ALICE" false 2>/dev/null + +echo "Authorizing and storing data..." +output=$(node "$SCRIPT_DIR/authorize-and-store.js" "$PARACHAIN_ENDPOINT" "//Alice" "$TEST_DATA" 2>/dev/null) +echo "$output" +CID=$(echo "$output" | grep "OUTPUT_CID=" | cut -d= -f2) + +echo "Verifying authorization..." +node "$SCRIPT_DIR/check-authorization.js" "$PARACHAIN_ENDPOINT" "$ALICE" true 2>/dev/null + +echo "Verifying data via IPFS..." +sleep 10 +node "$SCRIPT_DIR/verify-ipfs.js" "$CID" "$TEST_DATA" "$IPFS_URL" 2>/dev/null + +echo "=== All tests passed! ===" diff --git a/integration-tests/tests/01-store-data/verify-ipfs.js b/integration-tests/tests/01-store-data/verify-ipfs.js new file mode 100644 index 000000000..92a986b2e --- /dev/null +++ b/integration-tests/tests/01-store-data/verify-ipfs.js @@ -0,0 +1,25 @@ +const [,, cid, expectedData, ipfsUrl] = process.argv; + +const { create } = await import('ipfs-http-client'); +const client = create({ url: ipfsUrl }); + +for (let i = 0; i < 10; i++) { + try { + const chunks = []; + for await (const chunk of client.cat(cid)) chunks.push(chunk); + const data = '0x' + Buffer.concat(chunks).toString('hex'); + + if (data === expectedData) { + console.log('✅ Data verified via IPFS'); + process.exit(0); + } + console.error(`❌ Data mismatch: got ${data}, expected ${expectedData}`); + process.exit(1); + } catch (e) { + console.log(`Waiting for IPFS... (${e.message})`); + await new Promise(r => setTimeout(r, 3000)); + } +} + +console.error('❌ IPFS verification timeout'); +process.exit(1); diff --git a/integration-tests/tests/01-store-data/wait-for-chain.js b/integration-tests/tests/01-store-data/wait-for-chain.js new file mode 100644 index 000000000..737ef2838 --- /dev/null +++ b/integration-tests/tests/01-store-data/wait-for-chain.js @@ -0,0 +1,19 @@ +import { ApiPromise, WsProvider } from '@polkadot/api'; + +const [,, endpoint, minBlocks = "1"] = process.argv; +const required = parseInt(minBlocks); + +const provider = new WsProvider(endpoint); +const api = await ApiPromise.create({ provider, noInitWarn: true }); + +while (true) { + const header = await api.rpc.chain.getHeader(); + const height = header.number.toNumber(); + if (height >= required) { + console.log(`✅ Chain ready at ${endpoint} (height: ${height})`); + await api.disconnect(); + process.exit(0); + } + await new Promise(r => setTimeout(r, 1000)); +} + diff --git a/integration-tests/tests/02-elastic-scaling/package.json b/integration-tests/tests/02-elastic-scaling/package.json new file mode 100644 index 000000000..922e69942 --- /dev/null +++ b/integration-tests/tests/02-elastic-scaling/package.json @@ -0,0 +1,8 @@ +{ + "type": "module", + "dependencies": { + "@polkadot/api": "^16.5.2", + "@polkadot/keyring": "^13.5.8", + "@polkadot/util-crypto": "^13.5.8" + } +} diff --git a/integration-tests/tests/02-elastic-scaling/run.sh b/integration-tests/tests/02-elastic-scaling/run.sh new file mode 100755 index 000000000..56f2e76f5 --- /dev/null +++ b/integration-tests/tests/02-elastic-scaling/run.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BULLETIN_REPO_PATH="${BULLETIN_REPO_PATH:-$(cd "$SCRIPT_DIR/../../.." && pwd)}" + +ZOMBIENET_CONFIG="$BULLETIN_REPO_PATH/zombienet/bulletin-westend-local.toml" +RELAY_ENDPOINT="ws://127.0.0.1:9942" +PARACHAIN_ENDPOINT="ws://127.0.0.1:10000" + +SPAWNED_PIDS=() +cleanup() { + for pid in "${SPAWNED_PIDS[@]}"; do kill -9 "$pid" 2>/dev/null || true; done + [ -n "$zombienet_pid" ] && pkill -9 -P "$zombienet_pid" 2>/dev/null || true +} +trap cleanup EXIT + +echo "=== Bulletin Westend Elastic Scaling Test ===" + +[ -f "$BULLETIN_REPO_PATH/zombienet/bulletin-westend-spec.json" ] || bash "$BULLETIN_REPO_PATH/scripts/create_bulletin_westend_spec.sh" +[ -d "$SCRIPT_DIR/node_modules" ] || (cd "$SCRIPT_DIR" && npm install) + +echo "Starting network..." +pushd "$BULLETIN_REPO_PATH" > /dev/null +"$ZOMBIENET_BINARY" -p native spawn "$ZOMBIENET_CONFIG" > "$TEST_DIR/logs/zombienet.log" 2>&1 & +zombienet_pid=$! +SPAWNED_PIDS+=($zombienet_pid) +popd > /dev/null + +echo "Waiting for relay chain..." +node "$SCRIPT_DIR/wait-for-chain.js" "$RELAY_ENDPOINT" 2>/dev/null +echo "Waiting for parachain..." +node "$SCRIPT_DIR/wait-for-chain.js" "$PARACHAIN_ENDPOINT" 2>/dev/null + +echo "Assigning extra cores for elastic scaling..." +bash "$BULLETIN_REPO_PATH/scripts/assign_cores.sh" "$RELAY_ENDPOINT" 1006 0 1 2>/dev/null + +echo "Waiting for cores to take effect..." +node "$SCRIPT_DIR/wait-for-chain.js" "$PARACHAIN_ENDPOINT" 3 2>/dev/null + +echo "Testing elastic scaling with data storage..." +node "$SCRIPT_DIR/store-and-check-blocktime.js" "$PARACHAIN_ENDPOINT" "//Alice" + +echo "=== Elastic scaling test passed! ===" diff --git a/integration-tests/tests/02-elastic-scaling/store-and-check-blocktime.js b/integration-tests/tests/02-elastic-scaling/store-and-check-blocktime.js new file mode 100644 index 000000000..cbe631512 --- /dev/null +++ b/integration-tests/tests/02-elastic-scaling/store-and-check-blocktime.js @@ -0,0 +1,65 @@ +import { ApiPromise, WsProvider, Keyring } from '@polkadot/api'; +import { cryptoWaitReady } from '@polkadot/util-crypto'; + +const [,, endpoint, seed] = process.argv; +const NUM_TRANSACTIONS = 10; +const MAX_AVG_BLOCK_TIME = 2500; + +await cryptoWaitReady(); +const api = await ApiPromise.create({ provider: new WsProvider(endpoint), noInitWarn: true }); +const pair = new Keyring({ type: 'sr25519' }).addFromUri(seed); + +// Authorize account +console.log('Authorizing account...'); +await api.tx.sudo.sudo(api.tx.transactionStorage.authorizeAccount(pair.address, NUM_TRANSACTIONS + 1, 65536 * NUM_TRANSACTIONS)).signAndSend(pair); +await new Promise(r => setTimeout(r, 7000)); + +// Start block time measurement +const blockTimes = []; +let lastBlockTime = null; + +const unsub = await api.rpc.chain.subscribeNewHeads((header) => { + const now = Date.now(); + if (lastBlockTime) { + const delta = now - lastBlockTime; + blockTimes.push(delta); + console.log(`Block #${header.number}: ${delta}ms`); + } else { + console.log(`Block #${header.number}: (first)`); + } + lastBlockTime = now; +}); + +// Submit transactions spaced 2 seconds apart to land in different blocks +console.log(`Submitting ${NUM_TRANSACTIONS} store transactions (2s apart)...`); +for (let i = 0; i < NUM_TRANSACTIONS; i++) { + const data = `0x${Buffer.from(`test-data-${i}`).toString('hex')}`; + await api.tx.transactionStorage.store(data).signAndSend(pair); + console.log(`Tx ${i} submitted`); + if (i < NUM_TRANSACTIONS - 1) { + await new Promise(r => setTimeout(r, 2000)); + } +} + +// Wait for last tx to be included +await new Promise(r => setTimeout(r, 5000)); + +unsub(); + +if (blockTimes.length < 3) { + console.error(`❌ Not enough blocks (only ${blockTimes.length} intervals)`); + await api.disconnect(); + process.exit(1); +} + +const avg = blockTimes.reduce((a, b) => a + b, 0) / blockTimes.length; +console.log(`Average block time: ${avg.toFixed(0)}ms over ${blockTimes.length} intervals`); + +if (avg > MAX_AVG_BLOCK_TIME) { + console.error(`❌ Block time too slow: ${avg.toFixed(0)}ms > ${MAX_AVG_BLOCK_TIME}ms`); + await api.disconnect(); + process.exit(1); +} + +console.log(`✅ Elastic scaling working - average block time: ${avg.toFixed(0)}ms`); +await api.disconnect(); diff --git a/integration-tests/tests/02-elastic-scaling/wait-for-chain.js b/integration-tests/tests/02-elastic-scaling/wait-for-chain.js new file mode 100644 index 000000000..737ef2838 --- /dev/null +++ b/integration-tests/tests/02-elastic-scaling/wait-for-chain.js @@ -0,0 +1,19 @@ +import { ApiPromise, WsProvider } from '@polkadot/api'; + +const [,, endpoint, minBlocks = "1"] = process.argv; +const required = parseInt(minBlocks); + +const provider = new WsProvider(endpoint); +const api = await ApiPromise.create({ provider, noInitWarn: true }); + +while (true) { + const header = await api.rpc.chain.getHeader(); + const height = header.number.toNumber(); + if (height >= required) { + console.log(`✅ Chain ready at ${endpoint} (height: ${height})`); + await api.disconnect(); + process.exit(0); + } + await new Promise(r => setTimeout(r, 1000)); +} + diff --git a/runtimes/bulletin-westend/src/lib.rs b/runtimes/bulletin-westend/src/lib.rs index 10ac5448a..63b810ec6 100644 --- a/runtimes/bulletin-westend/src/lib.rs +++ b/runtimes/bulletin-westend/src/lib.rs @@ -88,6 +88,22 @@ use xcm_runtime_apis::{ fees::Error as XcmPaymentApiError, }; +/// Build with an offset of 1 behind the relay chain. +const RELAY_PARENT_OFFSET: u32 = 1; + +/// The upper limit of how many parachain blocks are processed by the relay chain per +/// parent. Limits the number of blocks authored per slot. This determines the minimum +/// block time of the parachain: +/// `RELAY_CHAIN_SLOT_DURATION_MILLIS/BLOCK_PROCESSING_VELOCITY` +const BLOCK_PROCESSING_VELOCITY: u32 = 3; + +/// Maximum number of blocks simultaneously accepted by the Runtime, not yet included +/// into the relay chain. +const UNINCLUDED_SEGMENT_CAPACITY: u32 = (2 + RELAY_PARENT_OFFSET) * BLOCK_PROCESSING_VELOCITY + 1; + +/// Relay chain slot duration, in milliseconds. +const RELAY_CHAIN_SLOT_DURATION_MILLIS: u32 = 6000; + /// The address format for describing accounts. pub type Address = MultiAddress; @@ -383,7 +399,7 @@ impl cumulus_pallet_parachain_system::Config for Runtime { type ReservedXcmpWeight = ReservedXcmpWeight; type CheckAssociatedRelayNumber = RelayNumberMonotonicallyIncreases; type ConsensusHook = ConsensusHook; - type RelayParentOffset = ConstU32<0>; + type RelayParentOffset = ConstU32; } type ConsensusHook = cumulus_pallet_aura_ext::FixedVelocityConsensusHook< @@ -608,7 +624,7 @@ impl_runtime_apis! { impl cumulus_primitives_core::RelayParentOffsetApi for Runtime { fn relay_parent_offset() -> u32 { - 0 + RELAY_PARENT_OFFSET } } diff --git a/scripts/assign_cores.sh b/scripts/assign_cores.sh new file mode 100755 index 000000000..a159eea98 --- /dev/null +++ b/scripts/assign_cores.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# +# Assign extra cores to a parachain on the relay chain. +# +# Usage: +# ./assign_cores.sh +# +# Example: +# ./assign_cores.sh ws://localhost:9942 1006 0 1 2 + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CMD_DIR="${SCRIPT_DIR}/assign_cores" + +# Check if node is available +if ! command -v node &> /dev/null; then + echo "Error: Node.js is required but not installed." + echo "Please install Node.js and try again." + exit 1 +fi + +# Check arguments +if [ $# -lt 3 ]; then + echo "Usage: $0 " + echo "" + echo "Arguments:" + echo " relay_endpoint WebSocket endpoint of the relay chain (e.g., ws://localhost:9942)" + echo " para_id Parachain ID to assign cores to (e.g., 1006)" + echo " cores Space-separated list of core numbers to assign" + echo "" + echo "Example:" + echo " $0 ws://localhost:9942 1006 0 1 2" + exit 1 +fi + +# Install npm dependencies if node_modules doesn't exist +if [ ! -d "${CMD_DIR}/node_modules" ]; then + echo "Installing npm dependencies..." + pushd "${CMD_DIR}" > /dev/null + npm install + popd > /dev/null + echo "" +fi + +# Run the assign_cores script +echo "Assigning cores to parachain..." +node "${CMD_DIR}/assign_cores.js" "$@" diff --git a/scripts/assign_cores/assign_cores.js b/scripts/assign_cores/assign_cores.js new file mode 100644 index 000000000..5dfaeb8ae --- /dev/null +++ b/scripts/assign_cores/assign_cores.js @@ -0,0 +1,34 @@ +/** + * Assign cores to a parachain. + * Usage: node assign_cores.js + */ +const { ApiPromise, WsProvider, Keyring } = require("@polkadot/api"); + +const [,, endpoint, paraIdStr, ...coreStrs] = process.argv; +const paraId = parseInt(paraIdStr); +const cores = coreStrs.map(c => parseInt(c)); + +if (!endpoint || isNaN(paraId) || cores.length === 0) { + console.log("Usage: node assign_cores.js "); + process.exit(1); +} + +console.log("Assigning cores to parachain..."); + +const api = await ApiPromise.create({ provider: new WsProvider(endpoint), noInitWarn: true }); +const alice = new Keyring({ type: "sr25519" }).addFromUri("//Alice"); + +const calls = cores.map(core => api.tx.coretime.assignCore(core, 0, [[{ Task: paraId }, 57600]], null)); +const tx = api.tx.sudo.sudo(api.tx.utility.batch(calls)); + +await new Promise((resolve, reject) => { + tx.signAndSend(alice, ({ status, dispatchError }) => { + if (status.isFinalized) { + if (dispatchError) reject(new Error(dispatchError.toString())); + else resolve(); + } + }); +}); + +console.log("✅ Cores assigned successfully!"); +await api.disconnect(); diff --git a/scripts/assign_cores/package.json b/scripts/assign_cores/package.json new file mode 100644 index 000000000..015ce3959 --- /dev/null +++ b/scripts/assign_cores/package.json @@ -0,0 +1,15 @@ +{ + "name": "bulletin-cmd", + "version": "1.0.0", + "description": "Command scripts for Bulletin Chain setup and management", + "main": "assign_cores.js", + "scripts": { + "assign-cores": "node assign_cores.js" + }, + "dependencies": { + "@polkadot/api": "^16.5.2", + "@polkadot/keyring": "^13.5.8", + "@polkadot/util": "^13.5.8" + } +} + diff --git a/zombienet/bulletin-westend-local.toml b/zombienet/bulletin-westend-local.toml index 9fb0c7c3b..2808d7b59 100644 --- a/zombienet/bulletin-westend-local.toml +++ b/zombienet/bulletin-westend-local.toml @@ -1,12 +1,34 @@ -# To run the network, execute the following command: +# Bulletin Westend Local Network Configuration # -# cd -# ./scripts/create_bulletin_westend_spec.sh -# POLKADOT_BINARY_PATH=~/local_bridge_testing/bin/polkadot POLKADOT_PARACHAIN_BINARY_PATH=~/local_bridge_testing/bin/polkadot-parachain zombienet -p native spawn ./zombienet/bulletin-westend-local.toml +# To run the network, execute the following commands: +# +# 1. Build and create the chain spec: +# cd +# ./scripts/create_bulletin_westend_spec.sh +# +# 2. Spawn the zombienet: +# POLKADOT_BINARY_PATH=~/local_bridge_testing/bin/polkadot \ +# POLKADOT_PARACHAIN_BINARY_PATH=~/local_bridge_testing/bin/polkadot-parachain \ +# zombienet -p native spawn ./zombienet/bulletin-westend-local.toml +# +# 3. Assign cores to the bulletin parachain for block production: +# After the network is spawned and running, assign cores to enable block production: +# +# ./scripts/assign_cores.sh ws://localhost:9942 1006 0 1 2 3 +# +# This assigns cores 0, 1, 2, and 3 to parachain 1006 (bulletin). +# Without explicit core assignment, the parachain may not produce blocks. +# +# The relay chain is configured with 4 cores (num_cores = 4) to support +# multi-core parachain block production. [settings] node_spawn_timeout = 240 +[relaychain.genesis.runtimeGenesis.patch.configuration.config.scheduler_params] + max_validators_per_core = 1 + num_cores = 3 + [relaychain] default_command = "{{POLKADOT_BINARY_PATH}}" default_args = ["-lruntime=debug,xcm=trace"] @@ -28,6 +50,7 @@ balance = 2000000000000 id = 1006 chain_spec_path = "./zombienet/bulletin-westend-spec.json" cumulus_based = true +default_args = ["--authoring", "slot-based"] [[parachains.collators]] name = "bulletin-westend-collator-1" @@ -50,3 +73,14 @@ args = [ "--ipfs-server", "-lparachain=debug,runtime=trace,xcm=trace,bitswap=trace,sub-libp2p::bitswap=trace", ] + +[[parachains.collators]] +name = "bulletin-westend-collator-3" +command = "{{POLKADOT_PARACHAIN_BINARY_PATH}}" +validator = true +p2p_port = 12347 +rpc_port = 12346 +args = [ + "--ipfs-server", + "-lparachain=debug,runtime=trace,xcm=trace,bitswap=trace,sub-libp2p::bitswap=trace", +]