Skip to content

Commit 0a3b63f

Browse files
authored
Fixed smoldot westend integration test (#170)
# Key Fixes ## Smoldot Bootnode Issue - Problem: Smoldot light client couldn't connect to parachain nodes. Smoldot supports only WebSocket transport, but zombienet bootnodes used only TCP. - Fix: I added `convertBootNodeToWebSocket()` that converts TCP `multiaddrs` to WebSocket format using **convention** `ws_port = tcp_port + 1`. Zombienet nodes configured with `--listen-addr /ip4/0.0.0.0/tcp/{port+1}/ws` ## Transaction Indexing - Problem: Parachain wasn't properly indexing transactions for the `TransactionStorage` pallet, - Fix: Enabled transaction-index feature on cumulus-pallet-parachain-system: ` cumulus-pallet-parachain-system = { features = ["transaction-index"], workspace = true }` @bkontur ## CI apt-get 403 Errors - Problem: Microsoft package repos on Github runners returned `403 Forbidden` - Fix: Removed Microsoft repo lists before apt-get update ## Other Changes - Added waitForChainReady to verify chain accessibility - Refactored smoldot script for parachain support (relay + para chain specs)
1 parent 8ec1075 commit 0a3b63f

File tree

8 files changed

+172
-58
lines changed

8 files changed

+172
-58
lines changed

.github/workflows/integration-test.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ jobs:
4242
components: rust-src
4343
- name: Install system dependencies
4444
run: |
45+
sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list /etc/apt/sources.list.d/azure-cli.list
4546
sudo apt-get update
4647
sudo apt-get install -y protobuf-compiler libclang-dev
4748
@@ -112,6 +113,7 @@ jobs:
112113

113114
- name: Install system dependencies
114115
run: |
116+
sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list /etc/apt/sources.list.d/azure-cli.list
115117
sudo apt-get update
116118
sudo apt-get install -y protobuf-compiler libclang-dev
117119
@@ -163,17 +165,13 @@ jobs:
163165
just run-authorize-and-store "bulletin-westend-runtime" "ws"
164166
- name: Run authorize and store (PAPI, smoldot, Westend parachain)
165167
working-directory: examples
166-
# TODO: remove when smoldot for para is fixed
167-
continue-on-error: true
168168
run: |
169169
export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test"
170170
echo "TEST_DIR=$TEST_DIR" >> "$GITHUB_ENV"
171171
mkdir -p "$TEST_DIR"
172172
just run-authorize-and-store "bulletin-westend-runtime" "smoldot"
173173
- name: Run store chunked data + DAG-PB (PJS-API, RPC node, Westend parachain)
174174
working-directory: examples
175-
# TODO: tmp
176-
continue-on-error: true
177175
run: |
178176
export TEST_DIR="$(mktemp -d $GITHUB_WORKSPACE/bulletin-tests-run-XXXXX)/test"
179177
echo "TEST_DIR=$TEST_DIR" >> "$GITHUB_ENV"

examples/api.js

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -37,42 +37,61 @@ export const TX_MODE_IN_BLOCK = "in-block";
3737
export const TX_MODE_FINALIZED_BLOCK = "finalized-block";
3838
export const TX_MODE_IN_POOL = "in-tx-pool";
3939

40-
function waitForTransaction(tx, signer, txName, txMode = TX_MODE_IN_BLOCK) {
40+
const DEFAULT_TX_TIMEOUT_MS = 60_000; // 60 seconds or 10 blocks
41+
42+
const TX_MODE_CONFIG = {
43+
[TX_MODE_IN_BLOCK]: {
44+
match: (ev) => ev.type === "txBestBlocksState" && ev.found,
45+
log: (txName, ev) => `📦 ${txName} included in block: ${ev.block.hash}`,
46+
},
47+
[TX_MODE_IN_POOL]: {
48+
match: (ev) => ev.type === "broadcasted",
49+
log: (txName, ev) => `📦 ${txName} broadcasted with txHash: ${ev.txHash}`,
50+
},
51+
[TX_MODE_FINALIZED_BLOCK]: {
52+
match: (ev) => ev.type === "finalized",
53+
log: (txName, ev) => `📦 ${txName} included in finalized block: ${ev.block.hash}`,
54+
},
55+
};
56+
57+
function waitForTransaction(tx, signer, txName, txMode = TX_MODE_IN_BLOCK, timeoutMs = DEFAULT_TX_TIMEOUT_MS) {
58+
const config = TX_MODE_CONFIG[txMode];
59+
if (!config) {
60+
return Promise.reject(new Error(`Unhandled txMode: ${txMode}`));
61+
}
62+
4163
return new Promise((resolve, reject) => {
42-
const sub = tx.signSubmitAndWatch(signer).subscribe({
64+
let sub;
65+
let resolved = false;
66+
67+
const cleanup = () => {
68+
resolved = true;
69+
clearTimeout(timeoutId);
70+
if (sub) sub.unsubscribe();
71+
};
72+
73+
const timeoutId = setTimeout(() => {
74+
if (!resolved) {
75+
cleanup();
76+
reject(new Error(`${txName} transaction timed out after ${timeoutMs}ms waiting for ${txMode}`));
77+
}
78+
}, timeoutMs);
79+
80+
sub = tx.signSubmitAndWatch(signer).subscribe({
4381
next: (ev) => {
4482
console.log(`✅ ${txName} event:`, ev.type);
45-
switch (txMode) {
46-
case TX_MODE_IN_BLOCK:
47-
if (ev.type === "txBestBlocksState" && ev.found) {
48-
console.log(`📦 ${txName} included in block:`, ev.block.hash);
49-
sub.unsubscribe();
50-
resolve(ev);
51-
}
52-
break;
53-
case TX_MODE_IN_POOL:
54-
if (ev.type === "broadcasted") {
55-
console.log(`📦 ${txName} broadcasted with txHash:`, ev.txHash);
56-
sub.unsubscribe();
57-
resolve(ev);
58-
}
59-
break;
60-
case TX_MODE_FINALIZED_BLOCK:
61-
if (ev.type === "finalized") {
62-
console.log(`📦 ${txName} included in finalized block:`, ev.block.hash);
63-
sub.unsubscribe();
64-
resolve(ev);
65-
}
66-
break;
67-
68-
default:
69-
throw new Error("Unhandled txMode: " + txMode)
83+
if (!resolved && config.match(ev)) {
84+
console.log(config.log(txName, ev));
85+
cleanup();
86+
resolve(ev);
7087
}
7188
},
7289
error: (err) => {
7390
console.error(`❌ ${txName} error:`, err);
74-
sub.unsubscribe();
75-
reject(err);
91+
if (!resolved) {
92+
cleanup();
93+
reject(err);
94+
}
7695
},
7796
complete: () => {
7897
console.log(`✅ ${txName} complete!`);

examples/authorize_and_store_papi_smoldot.js

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { createClient } from 'polkadot-api';
55
import { getSmProvider } from 'polkadot-api/sm-provider';
66
import { cryptoWaitReady } from '@polkadot/util-crypto';
77
import { authorizeAccount, fetchCid, store } from './api.js';
8-
import { setupKeyringAndSigners } from './common.js';
8+
import { setupKeyringAndSigners, waitForChainReady } from './common.js';
99
import { cidFromBytes } from "./cid_dag_metadata.js";
1010
import { bulletin } from './.papi/descriptors/dist/index.mjs';
1111

@@ -14,53 +14,111 @@ const SYNC_WAIT_SEC = 15;
1414
const SMOLDOT_LOG_LEVEL = 3; // 0=off, 1=error, 2=warn, 3=info, 4=debug, 5=trace
1515
const HTTP_IPFS_API = 'http://127.0.0.1:8080' // Local IPFS HTTP gateway
1616

17+
const TCP_BOOTNODE_REGEX = /^(\/ip[46]\/[^/]+)\/tcp\/(\d+)\/p2p\/(.+)$/;
18+
const WS_BOOTNODE_REGEX = /\/tcp\/\d+\/ws\/p2p\//;
19+
20+
/**
21+
* Converts a TCP bootnode to WebSocket format for smoldot compatibility.
22+
* Uses convention: WebSocket port = TCP p2p_port + 1
23+
*
24+
* Example: /ip4/127.0.0.1/tcp/30333/p2p/PEER_ID -> /ip4/127.0.0.1/tcp/30334/ws/p2p/PEER_ID
25+
*/
26+
function convertBootNodeToWebSocket(addr) {
27+
// Already a WebSocket address
28+
if (WS_BOOTNODE_REGEX.test(addr)) {
29+
console.log(` ✅ Already WebSocket: ${addr.substring(0, 50)}...`);
30+
return addr;
31+
}
32+
33+
const match = addr.match(TCP_BOOTNODE_REGEX);
34+
if (match) {
35+
const [, hostPart, portStr, peerId] = match;
36+
const wsPort = parseInt(portStr, 10) + 1;
37+
console.log(` 📡 Converted: tcp/${portStr} -> tcp/${wsPort}/ws`);
38+
return `${hostPart}/tcp/${wsPort}/ws/p2p/${peerId}`;
39+
}
40+
41+
return null;
42+
}
43+
1744
function readChainSpec(chainspecPath) {
18-
const chainSpecContent = readFileSync(chainspecPath, 'utf8');
19-
const chainSpecObj = JSON.parse(chainSpecContent);
45+
const chainSpecObj = JSON.parse(readFileSync(chainspecPath, 'utf8'));
2046
chainSpecObj.protocolId = null;
47+
48+
const bootNodes = chainSpecObj.bootNodes || [];
49+
if (bootNodes.length === 0) {
50+
console.log(`⚠️ No bootnodes found in chain spec: ${chainspecPath}`);
51+
return JSON.stringify(chainSpecObj);
52+
}
53+
54+
console.log(`🔄 Converting ${bootNodes.length} bootnode(s) to WebSocket for smoldot...`);
55+
const wsBootNodes = bootNodes.map(convertBootNodeToWebSocket).filter(Boolean);
56+
57+
if (wsBootNodes.length > 0) {
58+
chainSpecObj.bootNodes = wsBootNodes;
59+
console.log(`✅ Using ${wsBootNodes.length} WebSocket bootnode(s)`);
60+
} else {
61+
console.log(`⚠️ No bootnodes could be converted to WebSocket`);
62+
}
63+
2164
return JSON.stringify(chainSpecObj);
2265
}
2366

2467
function initSmoldot() {
25-
const sd = smoldot.start({
68+
return smoldot.start({
2669
maxLogLevel: SMOLDOT_LOG_LEVEL,
2770
logCallback: (level, target, message) => {
28-
const levelNames = ['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE'];
29-
const levelName = levelNames[level - 1] || 'UNKNOWN';
71+
const levelName = ['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE'][level - 1] || 'UNKNOWN';
3072
console.log(`[smoldot:${levelName}] ${target}: ${message}`);
3173
}
3274
});
33-
return sd;
3475
}
3576

36-
async function createSmoldotClient(chainspecPath) {
37-
const chainSpec = readChainSpec(chainspecPath);
77+
async function createSmoldotClient(chainSpecPath, parachainSpecPath = null) {
3878
const sd = initSmoldot();
39-
const chain = await sd.addChain({ chainSpec });
40-
const client = createClient(getSmProvider(chain));
41-
42-
return { client, sd };
79+
80+
const mainChain = await sd.addChain({ chainSpec: readChainSpec(chainSpecPath) });
81+
console.log(`✅ Added main chain: ${chainSpecPath}`);
82+
83+
let targetChain = mainChain;
84+
if (parachainSpecPath) {
85+
targetChain = await sd.addChain({
86+
chainSpec: readChainSpec(parachainSpecPath),
87+
potentialRelayChains: [mainChain]
88+
});
89+
console.log(`✅ Added parachain: ${parachainSpecPath}`);
90+
}
91+
92+
return { client: createClient(getSmProvider(targetChain)), sd };
4393
}
4494

4595
async function main() {
4696
await cryptoWaitReady();
4797

48-
// Get chainspec path from command line argument
49-
const chainspecPath = process.argv[2];
50-
if (!chainspecPath) {
51-
console.error('❌ Error: Chainspec path is required as first argument');
52-
console.error('Usage: node authorize_and_store_papi_smoldot.js <chainspec-path>');
98+
// Get chainspec path from command line argument (required - main chain: relay for para, or solo)
99+
const chainSpecPath = process.argv[2];
100+
if (!chainSpecPath) {
101+
console.error('❌ Error: Chain spec path is required as first argument');
102+
console.error('Usage: node authorize_and_store_papi_smoldot.js <chain-spec-path> [parachain-spec-path]');
103+
console.error(' For parachains: <relay-chain-spec-path> <parachain-spec-path>');
104+
console.error(' For solochains: <solo-chain-spec-path>');
53105
process.exit(1);
54106
}
55107

108+
// Optional parachain chainspec path (only needed for parachains)
109+
const parachainSpecPath = process.argv[3] || null;
110+
56111
let sd, client, resultCode;
57112
try {
58113
// Init Smoldot PAPI client and typed api.
59-
({ client, sd } = await createSmoldotClient(chainspecPath));
114+
({ client, sd } = await createSmoldotClient(chainSpecPath, parachainSpecPath));
60115
console.log(`⏭️ Waiting ${SYNC_WAIT_SEC} seconds for smoldot to sync...`);
61116
// TODO: check better way, when smoldot is synced, maybe some RPC/runtime api that checks best vs finalized block?
62117
await new Promise(resolve => setTimeout(resolve, SYNC_WAIT_SEC * 1000));
118+
119+
console.log('🔍 Checking if chain is ready...');
63120
const bulletinAPI = client.getTypedApi(bulletin);
121+
await waitForChainReady(bulletinAPI);
64122

65123
// Signers.
66124
const { sudoSigner, whoSigner, whoAddress } = setupKeyringAndSigners('//Alice', '//Alice');

examples/common.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,28 @@ export class NonceManager {
118118
return current;
119119
}
120120
}
121+
122+
/**
123+
* Wait for a PAPI typed API chain to be ready by checking runtime constants.
124+
* Retries until the chain is ready or max retries reached.
125+
*/
126+
export async function waitForChainReady(typedApi, maxRetries = 10, retryDelayMs = 2000) {
127+
128+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
129+
try {
130+
// Check runtime constants to verify chain is accessible
131+
const version = typedApi.constants.System.Version;
132+
console.log(`✅ Chain is ready! Runtime: ${version.spec_name} v${version.spec_version}`);
133+
return true;
134+
} catch (error) {
135+
if (attempt < maxRetries) {
136+
console.log(`⏳ Chain not ready yet (attempt ${attempt}/${maxRetries}), retrying in ${retryDelayMs/1000}s... Error: ${error.message}`);
137+
await new Promise(resolve => setTimeout(resolve, retryDelayMs));
138+
} else {
139+
console.log(`⚠️ Chain readiness check failed after ${maxRetries} attempts. Proceeding anyway... Error: ${error.message}`);
140+
return false;
141+
}
142+
}
143+
}
144+
return false;
145+
}

examples/justfile

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,11 +387,14 @@ run-authorize-and-store runtime mode="ws": npm-install
387387
if [ "{{ mode }}" = "smoldot" ]; then
388388
# Set chainspec path based on runtime
389389
if [ "{{ runtime }}" = "bulletin-westend-runtime" ]; then
390-
CHAINSPEC_PATH="$TEST_DIR/bulletin-westend-collator-1/cfg/westend-local-1006.json"
391-
else # bulletin-polkadot-runtime
390+
# Parachain: relay chain (required) + parachain spec (optional)
391+
RELAY_CHAINSPEC_PATH="$TEST_DIR/bob/cfg/westend-local.json"
392+
PARACHAIN_CHAINSPEC_PATH="$TEST_DIR/bulletin-westend-collator-2/cfg/westend-local-1006.json"
393+
node $SCRIPT_NAME "$RELAY_CHAINSPEC_PATH" "$PARACHAIN_CHAINSPEC_PATH"
394+
else # bulletin-polkadot-runtime (solochain)
392395
CHAINSPEC_PATH="$TEST_DIR/bob/cfg/bulletin-polkadot-local.json"
396+
node $SCRIPT_NAME "$CHAINSPEC_PATH"
393397
fi
394-
node $SCRIPT_NAME "$CHAINSPEC_PATH"
395398
else
396399
node $SCRIPT_NAME
397400
fi

runtimes/bulletin-westend/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ xcm-runtime-apis = { workspace = true }
6767

6868
# Cumulus
6969
cumulus-pallet-aura-ext = { workspace = true }
70-
cumulus-pallet-parachain-system = { workspace = true }
70+
cumulus-pallet-parachain-system = { features = ["transaction-index"], workspace = true }
7171
cumulus-pallet-session-benchmarking = { workspace = true }
7272
cumulus-pallet-weight-reclaim = { workspace = true }
7373
cumulus-pallet-xcm = { workspace = true }

scripts/create_bulletin_westend_spec.sh

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,4 @@ chain-spec-builder create \
1414
-r ./target/release/wbuild/bulletin-westend-runtime/bulletin_westend_runtime.compact.compressed.wasm \
1515
named-preset local_testnet
1616

17-
mv chain_spec.json bulletin-westend-spec.json
18-
cp bulletin-westend-spec.json ./zombienet/bulletin-westend-spec.json
17+
mv chain_spec.json ./zombienet/bulletin-westend-spec.json

zombienet/bulletin-westend-local.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,20 @@ chain = "westend-local"
1515
[[relaychain.nodes]]
1616
name = "alice"
1717
validator = true
18+
p2p_port = 30333
1819
rpc_port = 9942
1920
balance = 2000000000000
21+
# WebSocket P2P on p2p_port + 1 for smoldot light client support
22+
args = ["--listen-addr", "/ip4/0.0.0.0/tcp/30334/ws"]
2023

2124
[[relaychain.nodes]]
2225
name = "bob"
2326
validator = true
27+
p2p_port = 30433
2428
rpc_port = 9943
2529
balance = 2000000000000
30+
# WebSocket P2P on p2p_port + 1 for smoldot light client support
31+
args = ["--listen-addr", "/ip4/0.0.0.0/tcp/30434/ws"]
2632

2733
[[parachains]]
2834
id = 1006
@@ -38,6 +44,9 @@ rpc_port = 10000
3844
args = [
3945
"--ipfs-server",
4046
"-lparachain=debug,runtime=trace,xcm=trace,bitswap=trace,sub-libp2p::bitswap=trace",
47+
# WebSocket P2P on p2p_port + 1 for smoldot light client support
48+
"--listen-addr",
49+
"/ip4/0.0.0.0/tcp/10002/ws",
4150
]
4251

4352
[[parachains.collators]]
@@ -49,4 +58,7 @@ rpc_port = 12346
4958
args = [
5059
"--ipfs-server",
5160
"-lparachain=debug,runtime=trace,xcm=trace,bitswap=trace,sub-libp2p::bitswap=trace",
61+
# WebSocket P2P on p2p_port + 1 for smoldot light client support
62+
"--listen-addr",
63+
"/ip4/0.0.0.0/tcp/12348/ws",
5264
]

0 commit comments

Comments
 (0)