datahaven/test/scripts/update-validator-set.ts
Gonza Montiel 7097767021
feat: contracts upgrade command (#463)
## Contracts upgrade command with simple version tracking

This PR aims to take the most minimal changes from #438 to make the
upgrade command available.
So it adds the `bun cli contracts upgrade` command for deploying a new
`DataHavenServiceManager` implementation and upgrading the proxy, and
includes a simple version tracking via a `contracts/VERSION` file.

### Contracts
**`DataHavenServiceManager.sol`**
- Added `_version` storage variable
- Added `DATAHAVEN_VERSION()` view function, 
- Added `updateVersion(string)` function gated by `onlyProxyAdmin`
- Added `VersionUpdated` event
- The version is set at initialization and updated atomically with proxy
upgrades via `upgradeAndCall`.
 
### CLI

**`bun cli contracts upgrade`** works in two modes: _dry-run_ or
_execute_.

**Dry-run (default)**

Deploys the new implementation on-chain (signed by the deployer key),
then prints a ready-to-submit JSON payload for the multisig to execute
the proxy upgrade. No AVS owner key required.

```bash
# Uses version from contracts/VERSION (standard workflow)
bun cli contracts upgrade --chain hoodi

# Override version for this upgrade only (warns if it differs from contracts/VERSION)
bun cli contracts upgrade --chain hoodi --target x.y.z
```

Example output:
```json
{
  "to": "0xProxyAdmin...",
  "value": "0",
  "data": "0x...",
  "description": "Upgrade ServiceManager proxy to 0xNewImpl... and set version to 1.1.0"
}
```

**Execute mode (`--execute`)**

Deploys the implementation and broadcasts the proxy upgrade + version
update in a single atomic `upgradeAndCall` transaction. Requires
`AVS_OWNER_PRIVATE_KEY`. Used mostly for testing.

```bash
  bun cli contracts upgrade --chain anvil --execute
```
---
### Expected flow
- Bump mannually contracts/VERSION (e.g., 1.1.0)
- Run bun cli contracts upgrade --chain anvil|hoodi|mainnet
2026-03-02 21:50:10 +01:00

133 lines
4.6 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
// Update validator set on DataHaven substrate chain
import { $ } from "bun";
import invariant from "tiny-invariant";
import { logger } from "../utils/index";
interface UpdateValidatorSetOptions {
rpcUrl: string;
targetEra?: bigint;
}
/**
* Sends the validator set to the DataHaven chain through Snowbridge
*
* @param options - Configuration options for update
* @param options.rpcUrl - The RPC URL to connect to
* @returns Promise resolving to true if validator set was sent successfully, false if skipped
*/
export const updateValidatorSet = async (options: UpdateValidatorSetOptions): Promise<boolean> => {
const { rpcUrl } = options;
// Validate RPC URL
invariant(rpcUrl, "❌ RPC URL is required");
// Get cast path for transactions
const { stdout: castPath } = await $`which cast`.quiet();
const castExecutable = castPath.toString().trim();
// Get the owner's private key for transaction signing from the .env
const ownerPrivateKey =
process.env.AVS_OWNER_PRIVATE_KEY ||
"0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e"; // Sixth pre-funded account from Anvil
// Get deployed contract addresses from the deployments file
const deploymentPath = path.resolve("../contracts/deployments/anvil.json");
if (!fs.existsSync(deploymentPath)) {
logger.error(`Deployment file not found: ${deploymentPath}`);
return false;
}
const deployments = JSON.parse(fs.readFileSync(deploymentPath, "utf8"));
// Prepare command to send validator set
const serviceManagerAddress = deployments.ServiceManager;
invariant(serviceManagerAddress, "ServiceManager address not found in deployments");
// Security Note: Private key is passed via PRIVATE_KEY env var (not in argv) to avoid exposure in process lists.
// Using cast to send the transaction
const executionFee = "100000000000000000"; // 0.1 ETH
const relayerFee = "200000000000000000"; // 0.2 ETH
const value = "300000000000000000"; // 0.3 ETH (sum of fees)
const targetEra = options.targetEra ?? 1n;
if (options.targetEra === undefined) {
logger.warn(
"No target era specified; defaulting to era 1. Use --target-era for already-running networks."
);
}
const sendCommand = `${castExecutable} send --private-key $PRIVATE_KEY --value ${value} ${serviceManagerAddress} "sendNewValidatorSetForEra(uint64,uint128,uint128)" ${targetEra} ${executionFee} ${relayerFee} --rpc-url ${rpcUrl}`;
logger.debug(`Running command: ${sendCommand}`);
const { exitCode, stderr } = await $`sh -c ${sendCommand}`
.env({ ...process.env, PRIVATE_KEY: ownerPrivateKey })
.nothrow()
.quiet();
if (exitCode !== 0) {
logger.error(`Failed to send validator set: ${stderr.toString()}`);
return false;
}
logger.success("Validator set sent to Snowbridge Gateway");
// Check if the validator set has been queued on the substrate side (placeholder)
logger.debug("Checking validator set on substrate chain (not implemented)");
/*
// PLACEHOLDER: Code to check if validator set has been queued on substrate
// This requires a connection to the DataHaven substrate node which is not available yet
// Example of what this might look like:
const substrateApi = await ApiPromise.create({ provider: new WsProvider('ws://localhost:9944') });
const validatorSetModule = substrateApi.query.validatorSet;
const queuedValidators = await validatorSetModule.queuedValidators();
if (queuedValidators.length === validators.length) {
logger.success('Validator set successfully queued on substrate chain');
} else {
logger.warn('Validator set not properly queued on substrate chain');
}
*/
return true;
};
// Allow script to be run directly with CLI arguments
if (import.meta.main) {
const args = process.argv.slice(2);
const options: {
rpcUrl?: string;
targetEra?: bigint;
} = {};
// Extract RPC URL
const rpcUrlIndex = args.indexOf("--rpc-url");
if (rpcUrlIndex !== -1 && rpcUrlIndex + 1 < args.length) {
options.rpcUrl = args[rpcUrlIndex + 1];
}
// Extract target era
const targetEraIndex = args.indexOf("--target-era");
if (targetEraIndex !== -1 && targetEraIndex + 1 < args.length) {
options.targetEra = BigInt(args[targetEraIndex + 1]);
}
// Check required parameters
if (!options.rpcUrl) {
console.error("Error: --rpc-url parameter is required");
process.exit(1);
}
// Run update
updateValidatorSet({
rpcUrl: options.rpcUrl,
targetEra: options.targetEra
}).catch((error) => {
console.error("Validator set update failed:", error);
process.exit(1);
});
}