-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathupgrade_runtime.js
More file actions
263 lines (220 loc) · 9.22 KB
/
upgrade_runtime.js
File metadata and controls
263 lines (220 loc) · 9.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
#!/usr/bin/env node
/**
* Runtime Upgrade Script for Bulletin Chain Networks
*
* Usage:
* node upgrade_runtime.js <seed> <wasm_path> [options]
* node upgrade_runtime.js --verify-only [--network <name>]
*
* Options:
* --network <name> Network: westend, paseo (default: westend)
* --rpc <url> Custom RPC endpoint (overrides network default)
* --method <type> Upgrade method: setCode, authorize (default: based on network)
* --verify-only Only verify current runtime version, don't upgrade
* (dry-run validation runs automatically before every upgrade)
*/
import { cryptoWaitReady, blake2AsU8a } from '@polkadot/util-crypto';
import { createClient } from 'polkadot-api';
import { getWsProvider } from 'polkadot-api/ws';
import { newSigner, toHex } from './common.js';
import fs from 'fs';
// --- Network configs ---
const NETWORKS = {
westend: {
rpc: 'wss://westend-bulletin-rpc.polkadot.io',
method: 'sudo',
},
paseo: {
rpc: 'wss://paseo-bulletin-rpc.polkadot.io',
method: 'sudo',
}
};
// --- Arg parsing ---
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
seed: null,
wasmPath: null,
network: 'westend',
rpc: null,
method: null,
verifyOnly: false,
};
let i = 0;
while (i < args.length) {
const arg = args[i];
if (arg === '--network' && args[i + 1]) { opts.network = args[++i]; }
else if (arg === '--rpc' && args[i + 1]) { opts.rpc = args[++i]; }
else if (arg === '--method' && args[i + 1]) { opts.method = args[++i]; }
else if (arg === '--verify-only') { opts.verifyOnly = true; }
else if (arg.startsWith('--')) { console.error(`Unknown option: ${arg}`); process.exit(1); }
else if (!opts.seed) { opts.seed = arg; }
else if (!opts.wasmPath) { opts.wasmPath = arg; }
i++;
}
return opts;
}
function resolveNetwork(opts) {
const net = NETWORKS[opts.network];
if (!net && !opts.rpc) {
console.error(`Unknown network: ${opts.network}. Available: ${Object.keys(NETWORKS).join(', ')}`);
process.exit(1);
}
return {
rpc: opts.rpc || net.rpc,
method: opts.method || net?.method || 'sudo',
};
}
// --- Chain queries ---
async function getChainInfo(client) {
const unsafeApi = client.getUnsafeApi();
const runtimeVersion = await unsafeApi.constants.System.Version();
let lastUpgrade = null;
try { lastUpgrade = await unsafeApi.query.System.LastRuntimeUpgrade(); } catch (_) {}
return { runtimeVersion, lastUpgrade };
}
function printChainInfo({ runtimeVersion, lastUpgrade }) {
console.log('\nRuntime Version:');
console.log(` spec_name: ${runtimeVersion.spec_name}`);
console.log(` spec_version: ${runtimeVersion.spec_version}`);
console.log(` impl_version: ${runtimeVersion.impl_version}`);
console.log(` authoring_version: ${runtimeVersion.authoring_version}`);
console.log(` transaction_version: ${runtimeVersion.transaction_version}`);
if (lastUpgrade) {
console.log('\nLast Runtime Upgrade:');
console.log(` spec_version: ${lastUpgrade.spec_version}`);
console.log(` spec_name: ${lastUpgrade.spec_name}`);
}
}
// --- Upgrade methods ---
async function upgradeWithSetCode(client, signer, wasmCode, signerAddress) {
const unsafeApi = client.getUnsafeApi();
const setCodeCall = unsafeApi.tx.System.set_code({
code: wasmCode,
}).decodedCall;
const tx = unsafeApi.tx.Sudo.sudo({ call: setCodeCall });
// Step 1: Mandatory dry-run validation
console.log('\nStep 1: Dry-run validation...');
try {
const fees = await tx.getEstimatedFees(signerAddress);
console.log(` Estimated fees: ${fees}`);
console.log(' Dry-run passed!');
} catch (error) {
throw new Error(`Dry-run failed, upgrade NOT submitted: ${error.message}`);
}
// Step 2: Submit
console.log('\nStep 2: Submitting sudo.sudo(system.setCode)...');
const result = await tx.signAndSubmit(signer);
console.log(`Success! Block: ${result.block.hash}`);
}
async function upgradeWithAuthorize(client, signer, wasmCode, codeHash, signerAddress) {
const unsafeApi = client.getUnsafeApi();
const hashHex = toHex(codeHash);
const authorizeCall = unsafeApi.tx.System.authorize_upgrade({
code_hash: hashHex,
}).decodedCall;
let authorizeTx;
try {
authorizeTx = unsafeApi.tx.Sudo.sudo({ call: authorizeCall });
console.log(' via sudo.sudo(system.authorize_upgrade)');
} catch (_) {
authorizeTx = unsafeApi.tx.System.authorize_upgrade({
code_hash: hashHex,
});
console.log(' via system.authorize_upgrade (requires governance origin)');
}
// Step 1: Mandatory dry-run validation
console.log(`\nStep 1: Dry-run validation for authorize_upgrade (hash: ${hashHex})...`);
try {
const fees = await authorizeTx.getEstimatedFees(signerAddress);
console.log(` Estimated fees: ${fees}`);
console.log(' Dry-run passed!');
} catch (error) {
throw new Error(`Dry-run failed, upgrade NOT submitted: ${error.message}`);
}
// Step 2: Authorize (needs sudo or governance origin)
console.log('\nStep 2: Submitting authorize_upgrade...');
const result1 = await authorizeTx.signAndSubmit(signer);
console.log(` Authorized! Block: ${result1.block.hash}`);
// Step 3: Apply as unsigned extrinsic (no signer/fees needed for the large WASM payload).
// apply_authorized_upgrade supports ValidateUnsigned in the runtime.
console.log('\nStep 3: Applying authorized upgrade (unsigned)...');
const applyTx = unsafeApi.tx.System.apply_authorized_upgrade({
code: wasmCode,
});
const bareExtrinsic = await applyTx.getBareTx();
const result2 = await client.submit(bareExtrinsic);
console.log(` Applied! Block: ${result2.block.hash}`);
}
// --- Verify ---
async function verifyUpgrade(client, previousVersion, maxAttempts = 30, intervalMs = 12000) {
console.log(`\nVerifying upgrade (waiting for version to change from ${previousVersion})...`);
console.log(`Will poll every ${intervalMs / 1000}s for up to ${maxAttempts} attempts (~${(maxAttempts * intervalMs / 60000).toFixed(0)} minutes).`);
console.log('On parachains, runtime upgrades are enacted after a delay of several blocks.\n');
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
await new Promise(resolve => setTimeout(resolve, intervalMs));
const { runtimeVersion } = await getChainInfo(client);
console.log(` [${attempt}/${maxAttempts}] spec_version: ${runtimeVersion.spec_version}`);
if (runtimeVersion.spec_version > previousVersion) {
console.log(`\nUpgrade successful! Runtime upgraded from ${previousVersion} to ${runtimeVersion.spec_version}`);
return;
}
}
console.log(`\nWarning: spec_version is still ${previousVersion} after ${maxAttempts} attempts.`);
console.log('The upgrade may still be pending enactment. Check manually with --verify-only.');
}
// --- Main ---
async function main() {
const opts = parseArgs();
const { rpc, method } = resolveNetwork(opts);
// -- Verify-only mode --
if (opts.verifyOnly) {
console.log(`Connecting to ${rpc}...`);
const client = createClient(getWsProvider(rpc));
try {
printChainInfo(await getChainInfo(client));
} finally {
client.destroy();
}
process.exit(0);
}
// -- Validate inputs --
if (!opts.seed || !opts.wasmPath) {
console.error('Missing required arguments: <seed> <wasm_path>');
console.error('Run with --help or see script header for usage.');
process.exit(1);
}
if (!fs.existsSync(opts.wasmPath)) {
console.error(`WASM file not found: ${opts.wasmPath}`);
process.exit(1);
}
// -- Prepare --
await cryptoWaitReady();
const { signer, address } = newSigner(opts.seed);
const wasmCode = fs.readFileSync(opts.wasmPath);
const codeHash = blake2AsU8a(wasmCode, 256);
console.log(`Signer: ${address}`);
console.log(`WASM: ${opts.wasmPath} (${(wasmCode.length / 1024 / 1024).toFixed(2)} MB)`);
console.log(`Hash: ${toHex(codeHash)}`);
console.log(`Network: ${opts.network} (${method})`);
// -- Connect & upgrade --
console.log(`\nConnecting to ${rpc}...`);
const client = createClient(getWsProvider(rpc));
try {
const { runtimeVersion: current } = await getChainInfo(client);
console.log(`Current runtime: ${current.spec_name} v${current.spec_version}`);
if (method === 'setCode') {
await upgradeWithSetCode(client, signer, wasmCode, address);
} else {
await upgradeWithAuthorize(client, signer, wasmCode, codeHash, address);
}
await verifyUpgrade(client, current.spec_version);
} catch (error) {
console.error('\nError:', error.message);
if (error.cause) console.error('Cause:', error.cause);
process.exit(1);
} finally {
client.destroy();
}
}
main();