perf: Batch runtime parameter updates to speed up E2E setup (#368)

## Summary
Optimizes the DataHaven parameter configuration during E2E test
infrastructure setup by batching multiple extrinsics into a single
transaction, reducing setup time.
## Problem
Setting runtime parameters required 5 separate
`Parameters.set_parameter` calls, each waiting for block finality. This
created unnecessary delays during infrastructure setup since each call
blocked sequentially.
## Solution
- **Batch parameter updates:** Combine all `Parameters.set_parameter
`calls into a single `Utility.batch_all` transaction wrapped in
`Sudo.sudo`.
- **~5× faster parameter setup:** Only wait for finality once instead of
5 separate times
- **Code simplification:** Refactored parameter handling code, removing
~190 lines of unnecessary abstractions and complexity
This commit is contained in:
Ahmad Kaouk 2025-12-22 15:57:32 +01:00 committed by GitHub
parent 9135770a1e
commit 9344e243cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 91 additions and 282 deletions

View file

@ -30,10 +30,7 @@ export const setParametersFromCollection = async ({
const rpcUrl = `ws://127.0.0.1:${DEFAULT_SUBSTRATE_WS_PORT}`;
const parametersSet = await setDataHavenParameters({
rpcUrl,
parametersFilePath
});
const parametersSet = await setDataHavenParameters(rpcUrl, parametersFilePath);
printDivider();
return parametersSet;

View file

@ -45,10 +45,11 @@ export const setDataHavenParameters = async (options: ParametersOptions): Promis
const rpcUrl = `ws://127.0.0.1:${launchedNetwork.getPublicWsPort()}`;
// Execute the parameter update
await setDataHavenParametersScript({
rpcUrl,
parametersFilePath
});
const success = await setDataHavenParametersScript(rpcUrl, parametersFilePath);
if (!success) {
throw new Error("Failed to set DataHaven parameters");
}
logger.success("DataHaven parameters set successfully");
};

View file

@ -4,145 +4,87 @@ import { createClient } from "polkadot-api";
import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat";
import { getWsProvider } from "polkadot-api/ws-provider/web";
import { getEvmEcdsaSigner, logger, SUBSTRATE_FUNDED_ACCOUNTS } from "utils";
import { type ParsedDataHavenParameter, parseJsonToParameters } from "utils/types";
// Interface for the options object of setDataHavenParameters
interface SetDataHavenParametersOptions {
rpcUrl: string;
parametersFilePath: string;
}
import { parseJsonToParameters } from "utils/types";
/**
* Sets DataHaven runtime parameters on the specified RPC URL from a JSON file.
*
* @param options - Configuration options for setting parameters
* @param options.rpcUrl - The RPC URL of the DataHaven node
* @param options.parametersFilePath - Path to the JSON file containing an array of parameters to set
* @returns Promise resolving to true if parameters were set successfully, false if skipped
*/
export const setDataHavenParameters = async (
options: SetDataHavenParametersOptions
rpcUrl: string,
parametersFilePath: string
): Promise<boolean> => {
const { rpcUrl, parametersFilePath } = options;
const parametersJson = await Bun.file(parametersFilePath).json();
const parameters = parseJsonToParameters(parametersJson).filter((p) => p.value !== undefined);
// Load parameters from the JSON file
let parameters: ParsedDataHavenParameter[];
try {
const parametersFile = Bun.file(parametersFilePath);
const parametersJson = await parametersFile.text();
// Parse and convert the parameters using our utility
parameters = parseJsonToParameters(JSON.parse(parametersJson));
if (parameters.length === 0) {
logger.warn("⚠️ The parameters file is empty. No parameters to set.");
return false;
}
} catch (error: any) {
logger.error(
`❌ Error reading or parsing parameters file at '${parametersFilePath}': ${error.message}`
);
throw error;
if (parameters.length === 0) {
logger.warn("⚠️ No parameters to set.");
return false;
}
const client = createClient(withPolkadotSdkCompat(getWsProvider(rpcUrl)));
const dhApi = client.getTypedApi(datahaven);
logger.trace("Substrate client created");
const signer = getEvmEcdsaSigner(SUBSTRATE_FUNDED_ACCOUNTS.ALITH.privateKey);
logger.trace("Signer created for SUDO (ALITH)");
let allSuccessful = true;
try {
for (const param of parameters) {
// TODO: Add a graceful way to print the value of the parameter, since it won't always be representable as a hex string
logger.info(
`🔧 Attempting to set parameter: ${param.name.toString()} = ${param.value.asHex()}`
);
const dhApi = client.getTypedApi(datahaven);
const signer = getEvmEcdsaSigner(SUBSTRATE_FUNDED_ACCOUNTS.ALITH.privateKey);
const setParameterArgs: any = {
key_value: {
type: "RuntimeConfig" as const,
value: {
type: param.name,
value: [param.value]
}
}
};
try {
const setParameterCall = dhApi.tx.Parameters.set_parameter(setParameterArgs);
logger.debug("Parameter set call:");
logger.debug(setParameterCall.decodedCall);
const sudoCall = dhApi.tx.Sudo.sudo({
call: setParameterCall.decodedCall
});
logger.debug(`Submitting transaction to set ${String(param.name)}...`);
const txFinalisedPayload = await sudoCall.signAndSubmit(signer);
if (!txFinalisedPayload.ok) {
logger.error(
`❌ Transaction to set parameter ${String(param.name)} failed. Block: ${txFinalisedPayload.block.hash}, Tx Hash: ${txFinalisedPayload.txHash}`
);
logger.error(`Events: ${JSON.stringify(txFinalisedPayload.events)}`);
allSuccessful = false;
}
} catch (txError: any) {
logger.error(
`❌ Error submitting transaction for parameter ${String(param.name)}: ${txError.message || txError}`
);
allSuccessful = false;
}
// Log parameters being set
for (const p of parameters) {
logger.debug(`🔧 Setting ${p.name} = ${p.value!.asHex()}`);
}
// Build parameter calls
const calls = parameters.map(
(p) =>
dhApi.tx.Parameters.set_parameter({
key_value: {
type: "RuntimeConfig",
value: { type: p.name, value: [p.value] }
}
}).decodedCall
);
// Batch all calls and wrap in sudo
const tx = dhApi.tx.Sudo.sudo({
call: dhApi.tx.Utility.batch_all({ calls }).decodedCall
});
const result = await tx.signAndSubmit(signer);
if (!result.ok) {
logger.error(`❌ Transaction failed: ${result.block.hash}`);
return false;
}
logger.success("Runtime parameters set successfully");
return true;
} catch (error) {
logger.error(`${error instanceof Error ? error.message : error}`);
return false;
} finally {
client.destroy();
logger.trace("Substrate client destroyed");
}
if (allSuccessful) {
logger.success("All specified DataHaven parameters processed successfully.");
} else {
logger.warn("Some DataHaven parameters could not be set. Please check logs.");
}
return allSuccessful;
};
// Allow script to be run directly with CLI arguments
// CLI entry point
if (import.meta.main) {
const { values } = parseArgs({
args: process.argv,
options: {
rpcUrl: {
type: "string",
short: "r"
},
parametersFile: {
type: "string",
short: "f"
}
rpcUrl: { type: "string", short: "r" },
parametersFile: { type: "string", short: "f" }
},
strict: true
});
if (!values.rpcUrl) {
console.error("Error: --rpc-url parameter is required");
if (!values.rpcUrl || !values.parametersFile) {
console.error("Usage: --rpc-url <url> --parameters-file <path>");
process.exit(1);
}
if (!values.parametersFile) {
console.error("Error: --parameters-file <path_to_json_file> parameter is required.");
process.exit(1);
}
setDataHavenParameters({
rpcUrl: values.rpcUrl,
parametersFilePath: values.parametersFile
}).catch((error: Error) => {
console.error("Setting DataHaven parameters failed:", error.message || error);
process.exit(1);
});
setDataHavenParameters(values.rpcUrl, values.parametersFile)
.then((ok) => process.exit(ok ? 0 : 1))
.catch((e) => {
console.error(e);
process.exit(1);
});
}

View file

@ -1,7 +1,13 @@
import path from "node:path";
import { $ } from "bun";
import { logger } from "./logger";
import type { ParsedDataHavenParameter } from "./types";
import type { DataHavenRuntimeParameterKey } from "./types";
/** Raw parameter for JSON storage (hex strings, not FixedSizeBinary) */
interface RawParameter {
name: DataHavenRuntimeParameterKey;
value: string | null | undefined;
}
// Constants for paths
export const PARAMETERS_TEMPLATE_PATH = "configs/parameters/datahaven-parameters.json";
@ -15,13 +21,13 @@ export const PARAMETERS_OUTPUT_PATH = path.join(PARAMETERS_OUTPUT_DIR, PARAMETER
* and then generate a JSON file to be used by the setDataHavenParameters script.
*/
export class ParameterCollection {
private parameters: ParsedDataHavenParameter[] = [];
private parameters: RawParameter[] = [];
/**
* Adds a parameter to the collection
* @param param The parameter to add
*/
public addParameter(param: ParsedDataHavenParameter): void {
public addParameter(param: RawParameter): void {
// Check if parameter with same name already exists
const existingIndex = this.parameters.findIndex((p) => p.name === param.name);
if (existingIndex !== -1) {
@ -38,7 +44,7 @@ export class ParameterCollection {
/**
* Returns the current parameters
*/
public getParameters(): ParsedDataHavenParameter[] {
public getParameters(): RawParameter[] {
return [...this.parameters];
}

View file

@ -180,174 +180,37 @@ export const parseJsonToBeaconCheckpoint = (jsonInput: any): BeaconCheckpoint =>
}
};
/**
* The key of the DataHaven runtime parameter.
* This is an union type with all the possible parameter keys.
*/
export type DataHavenRuntimeParameterKey =
| "EthereumGatewayAddress"
| "RewardsRegistryAddress"
| "RewardsUpdateSelector"
| "RewardsAgentOrigin"
| "DatahavenServiceManagerAddress";
/** Valid parameter names for DataHaven runtime configuration */
const DATAHAVEN_PARAM_NAMES = [
"EthereumGatewayAddress",
"RewardsRegistryAddress",
"RewardsUpdateSelector",
"RewardsAgentOrigin",
"DatahavenServiceManagerAddress"
] as const;
/**
* Interface for raw JSON parameters before conversion
*/
export interface RawJsonParameter {
name: DataHavenRuntimeParameterKey;
value: string | null | undefined;
}
export type DataHavenRuntimeParameterKey = (typeof DATAHAVEN_PARAM_NAMES)[number];
/**
* Schema for raw EthereumGatewayAddress parameter
*/
const rawEthereumGatewayAddressSchema = z.object({
name: z.literal("EthereumGatewayAddress"),
/** Schema for a single parameter: { name, value } */
const parameterSchema = z.object({
name: z.enum(DATAHAVEN_PARAM_NAMES),
value: hexStringSchema.nullable().optional()
});
/**
* Schema for raw RewardsRegistryAddress parameter
*/
const rawRewardsRegistryAddressSchema = z.object({
name: z.literal("RewardsRegistryAddress"),
value: hexStringSchema.nullable().optional()
});
/**
* Schema for raw RewardsUpdateSelector parameter
*/
const rawRewardsUpdateSelectorSchema = z.object({
name: z.literal("RewardsUpdateSelector"),
value: hexStringSchema.nullable().optional()
});
/**
* Schema for raw RewardsOrigin parameter
*/
const rawRewardsAgentOriginSchema = z.object({
name: z.literal("RewardsAgentOrigin"),
value: hexStringSchema.nullable().optional()
});
/**
* Schema for raw DatahavenServiceManagerAddress parameter
*/
const rawDatahavenServiceManagerAddressSchema = z.object({
name: z.literal("DatahavenServiceManagerAddress"),
value: hexStringSchema.nullable().optional()
});
/**
* Union schema for raw DataHaven parameters (for parsing JSON)
*/
export const rawDataHavenParameterSchema = z.discriminatedUnion("name", [
rawEthereumGatewayAddressSchema,
rawRewardsRegistryAddressSchema,
rawRewardsUpdateSelectorSchema,
rawRewardsAgentOriginSchema,
rawDatahavenServiceManagerAddressSchema
]);
/**
* Schema for an array of raw DataHaven parameters
*/
export const rawDataHavenParametersArraySchema = z.array(rawDataHavenParameterSchema);
/**
* The parsed type of a DataHaven runtime parameter.
*/
export interface ParsedDataHavenParameter {
name: DataHavenRuntimeParameterKey;
value: any;
value: FixedSizeBinary<number> | undefined;
}
/**
* Converts a parsed raw parameter to its typed version
*/
function convertParameter(rawParam: any): ParsedDataHavenParameter {
if (rawParam.name === "EthereumGatewayAddress" && rawParam.value) {
return {
name: rawParam.name,
value: new FixedSizeBinary<20>(hexToUint8Array(rawParam.value))
};
}
if (rawParam.name === "RewardsRegistryAddress" && rawParam.value) {
return {
name: rawParam.name,
value: new FixedSizeBinary<20>(hexToUint8Array(rawParam.value))
};
}
if (rawParam.name === "RewardsUpdateSelector" && rawParam.value) {
return {
name: rawParam.name,
value: new FixedSizeBinary<4>(hexToUint8Array(rawParam.value))
};
}
if (rawParam.name === "RewardsAgentOrigin" && rawParam.value) {
return {
name: rawParam.name,
value: new FixedSizeBinary<32>(hexToUint8Array(rawParam.value))
};
}
if (rawParam.name === "DatahavenServiceManagerAddress" && rawParam.value) {
return {
name: rawParam.name,
value: new FixedSizeBinary<20>(hexToUint8Array(rawParam.value))
};
}
// For other parameter types, add conversion logic here
return rawParam;
}
/**
* Parses and converts a JSON object into a typed DataHaven parameter.
*
* @param jsonInput - The JSON parameter object to parse.
* @returns The parsed and converted parameter.
*/
export const parseJsonToParameter = (jsonInput: any): ParsedDataHavenParameter => {
try {
// First validate the raw structure
const rawParam = rawDataHavenParameterSchema.parse(jsonInput);
// Then convert to typed version
return convertParameter(rawParam);
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(
`Invalid JSON structure for DataHaven parameter: ${error.errors
.map((e) => `${e.path.join(".")} - ${e.message}`)
.join(", ")}`
);
}
throw error;
}
};
/**
* Parses and converts an array of JSON parameters into typed DataHaven parameters.
*
* @param jsonInput - Array of JSON parameter objects to parse.
* @returns Array of parsed and converted parameters.
*/
export const parseJsonToParameters = (jsonInput: any[]): ParsedDataHavenParameter[] => {
try {
// First validate the raw structure of all parameters
const rawParams = rawDataHavenParametersArraySchema.parse(jsonInput);
// Then convert each parameter to its typed version
return rawParams.map(convertParameter);
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(
`Invalid JSON structure for DataHaven parameters array: ${error.errors
.map((e) => `${e.path.join(".")} - ${e.message}`)
.join(", ")}`
);
}
throw error;
}
/** Parses JSON array into typed parameters with FixedSizeBinary values */
export const parseJsonToParameters = (jsonInput: unknown[]): ParsedDataHavenParameter[] => {
return z
.array(parameterSchema)
.parse(jsonInput)
.map((p) => ({
name: p.name,
value: p.value ? new FixedSizeBinary(hexToUint8Array(p.value)) : undefined
}));
};
/**