mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
## Summary
- Add multi-environment deployment support (stagenet, testnet, mainnet)
to CLI and contracts
- Configure stagenet and testnet runtimes with correct genesis hashes
and Snowbridge Agent IDs
- Add CLI commands for BEEFY checkpoint updates and rewards origin
computation
- Add ETH validator strategies (native beacon chain ETH + LSTs) to all
config files
## Changes
### Runtime Configuration
**Stagenet Runtime:**
- Set `StagenetGenesisHash` to DataHaven stagenet genesis hash
- Configure `RewardsAgentOrigin` with computed Snowbridge Agent ID
- Add tests verifying rewards account derivation and agent ID
computation
**Testnet Runtime:**
- Set `TestnetGenesisHash` to DataHaven testnet genesis hash
- Configure `RewardsAgentOrigin` with computed Snowbridge Agent ID
- Add tests verifying rewards account derivation and agent ID
computation
The Rewards Agent ID is computed following Snowbridge's location
description pattern:
```
blake2_256(SCALE_ENCODE("GlobalConsensus", ByGenesis(genesis), "AccountKey20", rewards_account))
```
### CLI Enhancements
- All contracts subcommands (`status`, `deploy`, `verify`,
`update-metadata`) now accept `--environment` option
- Config and deployment files use environment-prefixed naming (e.g.,
`stagenet-hoodi.json`, `testnet-hoodi.json`)
- New `update-beefy-checkpoint` command that:
- Connects to a live DataHaven chain via WebSocket RPC
- Fetches all BEEFY data at the same finalized block for consistency
- Uses parallel queries with `Promise.all` for better performance
- Computes authority hashes (keccak256 of Ethereum addresses derived
from BEEFY public keys)
- Uses Snowbridge's quorum formula `n - floor((n-1)/3)` for strictly >
2/3 majority
- New `update-rewards-origin` command that computes the Snowbridge Agent
ID for the rewards pallet
- Centralized validation via `contractsPreActionHook` for all contract
commands
- Environment validation against allowlist (`stagenet`, `testnet`,
`mainnet`)
### Contract Changes
- Network validation uses explicit allowlist instead of suffix matching
- Added `initialValidatorSetId` and `nextValidatorSetId` fields to
`SnowbridgeConfig` struct
- `DeployBase.s.sol` now uses config values for validator set IDs
instead of hardcoded 0/1
- `DeployParams.s.sol` loads validator set IDs from config with
backwards compatibility
### Validator Strategies
Added ETH-equivalent strategies to allow validators to stake using
native ETH or LSTs:
**All Networks:**
- `0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0` - Native beacon chain ETH
(virtual strategy)
**Hoodi Testnet:**
- `0xf8a1a66130d614c7360e868576d5e59203475fe0` - stETH
- `0x24579aD4fe83aC53546E5c2D3dF5F85D6383420d` - WETH
**Ethereum Mainnet:**
- `0x93c4b944D05dfe6df7645A86cd2206016c51564D` - stETH
- `0x1BeE69b7dFFfA4E2d53C2a2Df135C388AD25dCD2` - rETH
- `0x54945180dB7943c0ed0FEE7EdaB2Bd24620256bc` - cbETH
### Config Files
- `stagenet-hoodi.json` - Hoodi testnet with stagenet EigenLayer
addresses
- `testnet-hoodi.json` - Hoodi testnet with testnet EigenLayer addresses
- `mainnet-ethereum.json` - Ethereum mainnet with mainnet EigenLayer
addresses
- Removed `hoodi.json` (replaced by environment-prefixed files)
## Usage
```bash
# Deploy to stagenet on Hoodi
bun cli contracts deploy --chain hoodi --environment stagenet
# Update BEEFY checkpoint from live chain
bun cli contracts update-beefy-checkpoint \
--chain hoodi \
--environment stagenet \
--rpc-url wss://services.datahaven-dev.network/stagenet
# Compute rewards origin for a chain
bun cli contracts update-rewards-origin \
--chain hoodi \
--environment stagenet \
--rpc-url wss://services.datahaven-dev.network/stagenet
# Check deployment status
bun cli contracts status --chain hoodi --environment stagenet
```
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
356 lines
11 KiB
TypeScript
356 lines
11 KiB
TypeScript
import { $ } from "bun";
|
|
import { CHAIN_CONFIGS, loadChainConfig } from "configs/contracts/config";
|
|
import invariant from "tiny-invariant";
|
|
import { logger, parseDeploymentsFile, runShellCommandWithLogger } from "utils";
|
|
import type { ParameterCollection } from "utils/parameters";
|
|
import { encodeFunctionData } from "viem";
|
|
import { privateKeyToAccount } from "viem/accounts";
|
|
import { dataHavenServiceManagerAbi } from "../contract-bindings/generated";
|
|
|
|
interface ContractDeploymentOptions {
|
|
chain?: string;
|
|
environment?: string;
|
|
rpcUrl?: string;
|
|
privateKey?: string | undefined;
|
|
verified?: boolean;
|
|
blockscoutBackendUrl?: string;
|
|
avsOwnerAddress?: string;
|
|
avsOwnerKey?: string;
|
|
txExecution?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Builds the network identifier from chain and optional environment
|
|
* When environment is specified: {environment}-{chain} (e.g., "stagenet-hoodi")
|
|
* When environment is not specified: {chain} (e.g., "hoodi")
|
|
*/
|
|
export const buildNetworkId = (chain: string, environment?: string): string => {
|
|
return environment ? `${environment}-${chain}` : chain;
|
|
};
|
|
|
|
/**
|
|
* Validates deployment parameters
|
|
*/
|
|
export const validateDeploymentParams = (options: ContractDeploymentOptions) => {
|
|
const { rpcUrl, verified, blockscoutBackendUrl } = options;
|
|
|
|
invariant(rpcUrl, "❌ RPC URL is required");
|
|
if (verified) {
|
|
invariant(blockscoutBackendUrl, "❌ Blockscout backend URL is required for verification");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Builds smart contracts using forge
|
|
*/
|
|
export const buildContracts = async () => {
|
|
logger.info("🛳️ Building contracts...");
|
|
const {
|
|
exitCode: buildExitCode,
|
|
stderr: buildStderr,
|
|
stdout: buildStdout
|
|
} = await $`forge build`.cwd("../contracts").nothrow().quiet();
|
|
|
|
if (buildExitCode !== 0) {
|
|
logger.error(buildStderr.toString());
|
|
throw Error("❌ Contracts have failed to build properly.");
|
|
}
|
|
logger.debug(buildStdout.toString());
|
|
};
|
|
|
|
/**
|
|
* Constructs the deployment command
|
|
*/
|
|
export const constructDeployCommand = (options: ContractDeploymentOptions): string => {
|
|
const { chain, environment, rpcUrl, verified, blockscoutBackendUrl } = options;
|
|
|
|
const deploymentScript =
|
|
!chain || chain === "anvil"
|
|
? "script/deploy/DeployLocal.s.sol"
|
|
: "script/deploy/DeployLive.s.sol";
|
|
|
|
// Build the network identifier for display and environment variable
|
|
const networkId = buildNetworkId(chain || "anvil", environment);
|
|
|
|
logger.info(`🚀 Deploying contracts to ${networkId} using ${deploymentScript}`);
|
|
|
|
let deployCommand = `forge script ${deploymentScript} --rpc-url ${rpcUrl} --color never -vv --no-rpc-rate-limit --non-interactive --broadcast`;
|
|
|
|
// Add environment variables for network (used by Solidity scripts for config/output file naming)
|
|
if (chain) {
|
|
deployCommand = `NETWORK=${networkId} ${deployCommand}`;
|
|
}
|
|
|
|
if (verified && blockscoutBackendUrl) {
|
|
// TODO: Allow for other verifiers like Etherscan.
|
|
deployCommand += ` --verify --verifier blockscout --verifier-url ${blockscoutBackendUrl}/api/ --delay 0`;
|
|
logger.info("🔍 Contract verification enabled");
|
|
}
|
|
|
|
return deployCommand;
|
|
};
|
|
|
|
/**
|
|
* Executes contract deployment
|
|
* Supports multiple calling patterns for backwards compatibility:
|
|
*/
|
|
export const executeDeployment = async (
|
|
deployCommand: string,
|
|
parameterCollection?: ParameterCollection,
|
|
chain?: string,
|
|
env?: Record<string, string>
|
|
) => {
|
|
logger.info("⌛️ Deploying contracts (this might take a few minutes)...");
|
|
|
|
// Using custom shell command to improve logging with forge's stdoutput
|
|
await runShellCommandWithLogger(deployCommand, {
|
|
cwd: "../contracts",
|
|
env
|
|
});
|
|
|
|
// After deployment, read the Gateway address and add it to parameters if collection is provided
|
|
if (parameterCollection) {
|
|
await updateParameters(parameterCollection, chain);
|
|
}
|
|
|
|
logger.success("Contracts deployed successfully");
|
|
};
|
|
|
|
/**
|
|
* Read the parameters from the deployed contracts and add it to the collection.
|
|
*/
|
|
export const updateParameters = async (
|
|
parameterCollection: ParameterCollection,
|
|
chain?: string
|
|
) => {
|
|
try {
|
|
const deployments = await parseDeploymentsFile(chain);
|
|
const gatewayAddress = deployments.Gateway;
|
|
const serviceManagerAddress = deployments.ServiceManager;
|
|
|
|
if (gatewayAddress) {
|
|
logger.debug(`📝 Adding EthereumGatewayAddress parameter: ${gatewayAddress}`);
|
|
|
|
parameterCollection.addParameter({
|
|
name: "EthereumGatewayAddress",
|
|
value: gatewayAddress
|
|
});
|
|
} else {
|
|
logger.warn("⚠️ Gateway address not found in deployments file");
|
|
}
|
|
|
|
if (serviceManagerAddress) {
|
|
logger.debug(`📝 Adding DatahavenServiceManagerAddress parameter: ${serviceManagerAddress}`);
|
|
parameterCollection.addParameter({
|
|
name: "DatahavenServiceManagerAddress",
|
|
value: serviceManagerAddress
|
|
});
|
|
} else {
|
|
logger.warn("⚠️ ServiceManager address not found in deployments file");
|
|
}
|
|
} catch (error) {
|
|
logger.error(`Failed to read parameters from deployment: ${error}`);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Main function to deploy contracts with simplified interface
|
|
* This is the main entry point for CLI handlers
|
|
*/
|
|
export const deployContracts = async (options: {
|
|
chain: string;
|
|
environment?: string;
|
|
rpcUrl?: string;
|
|
privateKey?: string | undefined;
|
|
verified?: boolean;
|
|
blockscoutBackendUrl?: string;
|
|
avsOwnerKey?: string;
|
|
avsOwnerAddress?: string;
|
|
txExecution?: boolean;
|
|
}) => {
|
|
const chainConfig = CHAIN_CONFIGS[options.chain as keyof typeof CHAIN_CONFIGS];
|
|
|
|
if (!chainConfig) {
|
|
throw new Error(`Unsupported chain: ${options.chain}`);
|
|
}
|
|
|
|
// Build network identifier for config/deployment file naming
|
|
const networkId = buildNetworkId(options.chain, options.environment);
|
|
|
|
const finalRpcUrl = options.rpcUrl || chainConfig.RPC_URL;
|
|
const isLocalChain = options.chain === "anvil";
|
|
const txExecutionEnabled = options.txExecution ?? isLocalChain;
|
|
const normalizedOwnerKey = normalizePrivateKey(
|
|
options.avsOwnerKey || process.env.AVS_OWNER_PRIVATE_KEY
|
|
);
|
|
|
|
let resolvedAvsOwnerAddress = options.avsOwnerAddress;
|
|
if (!resolvedAvsOwnerAddress && normalizedOwnerKey) {
|
|
resolvedAvsOwnerAddress = privateKeyToAccount(normalizedOwnerKey).address;
|
|
}
|
|
|
|
if (!resolvedAvsOwnerAddress && isLocalChain) {
|
|
const config = await loadChainConfig(options.chain, options.environment);
|
|
resolvedAvsOwnerAddress = config?.avs?.avsOwner;
|
|
}
|
|
|
|
if (!resolvedAvsOwnerAddress) {
|
|
throw new Error(
|
|
"AVS owner address is required. Provide --avs-owner-address, --avs-owner-key, or AVS_OWNER_ADDRESS."
|
|
);
|
|
}
|
|
|
|
if (txExecutionEnabled && !normalizedOwnerKey) {
|
|
throw new Error(
|
|
"Executing AVS owner transactions requires --avs-owner-key or AVS_OWNER_PRIVATE_KEY to be set."
|
|
);
|
|
}
|
|
|
|
const deploymentOptions: ContractDeploymentOptions = {
|
|
chain: options.chain,
|
|
environment: options.environment,
|
|
rpcUrl: finalRpcUrl,
|
|
privateKey: options.privateKey,
|
|
verified: options.verified,
|
|
blockscoutBackendUrl: options.blockscoutBackendUrl,
|
|
avsOwnerAddress: resolvedAvsOwnerAddress,
|
|
avsOwnerKey: normalizedOwnerKey,
|
|
txExecution: txExecutionEnabled
|
|
};
|
|
|
|
// Validate parameters
|
|
validateDeploymentParams(deploymentOptions);
|
|
|
|
// Build contracts
|
|
await buildContracts();
|
|
|
|
// Construct and execute deployment
|
|
const deployCommand = constructDeployCommand(deploymentOptions);
|
|
const env = buildDeploymentEnv(deploymentOptions);
|
|
await executeDeployment(deployCommand, undefined, networkId, env);
|
|
|
|
if (!txExecutionEnabled) {
|
|
await emitOwnerTransactionCalldata(networkId);
|
|
}
|
|
|
|
logger.success(`DataHaven contracts deployed successfully to ${networkId}`);
|
|
};
|
|
|
|
const normalizePrivateKey = (key?: string): `0x${string}` | undefined => {
|
|
if (!key) {
|
|
return undefined;
|
|
}
|
|
return (key.startsWith("0x") ? key : `0x${key}`) as `0x${string}`;
|
|
};
|
|
|
|
const buildDeploymentEnv = (options: ContractDeploymentOptions) => {
|
|
const env: Record<string, string> = {};
|
|
|
|
if (options.privateKey) {
|
|
env.DEPLOYER_PRIVATE_KEY = options.privateKey;
|
|
}
|
|
|
|
if (options.avsOwnerKey) {
|
|
env.AVS_OWNER_PRIVATE_KEY = options.avsOwnerKey;
|
|
}
|
|
|
|
if (options.avsOwnerAddress) {
|
|
env.AVS_OWNER_ADDRESS = options.avsOwnerAddress;
|
|
}
|
|
|
|
if (typeof options.txExecution === "boolean") {
|
|
env.TX_EXECUTION = options.txExecution ? "true" : "false";
|
|
}
|
|
|
|
return env;
|
|
};
|
|
|
|
const emitOwnerTransactionCalldata = async (chain?: string) => {
|
|
try {
|
|
const deployments = await parseDeploymentsFile(chain);
|
|
|
|
const serviceManager = deployments.ServiceManager;
|
|
|
|
if (!serviceManager) {
|
|
logger.warn("⚠️ Missing ServiceManager address; cannot produce multisig calldata.");
|
|
return;
|
|
}
|
|
|
|
const calls = [
|
|
{
|
|
label: "Set metadata URI",
|
|
description: 'DataHavenServiceManager.updateAVSMetadataURI("")',
|
|
to: serviceManager,
|
|
value: "0",
|
|
data: encodeFunctionData({
|
|
abi: dataHavenServiceManagerAbi,
|
|
functionName: "updateAVSMetadataURI",
|
|
args: [""]
|
|
})
|
|
}
|
|
];
|
|
|
|
logger.info(
|
|
"🔐 On-chain owner transactions were deferred. Submit the following calls via your multisig:"
|
|
);
|
|
calls.forEach((call, index) => {
|
|
logger.info(`\n#${index + 1} ${call.label}`);
|
|
logger.info(call.description);
|
|
logger.info(JSON.stringify(call, null, 2));
|
|
});
|
|
} catch (error) {
|
|
logger.warn(`⚠️ Failed to build multisig calldata: ${error}`);
|
|
}
|
|
};
|
|
|
|
// Allow script to be run directly with CLI arguments
|
|
if (import.meta.main) {
|
|
const args = process.argv.slice(2);
|
|
|
|
// Extract RPC URL
|
|
const rpcUrlIndex = args.indexOf("--rpc-url");
|
|
invariant(rpcUrlIndex !== -1, "❌ --rpc-url flag is required");
|
|
invariant(rpcUrlIndex + 1 < args.length, "❌ --rpc-url flag requires an argument");
|
|
|
|
// Extract private key
|
|
const privateKeyIndex = args.indexOf("--private-key");
|
|
invariant(privateKeyIndex !== -1, "❌ --private-key flag is required");
|
|
invariant(privateKeyIndex + 1 < args.length, "❌ --private-key flag requires an argument");
|
|
|
|
const options: {
|
|
rpcUrl: string;
|
|
privateKey: string;
|
|
verified: boolean;
|
|
blockscoutBackendUrl?: string;
|
|
} = {
|
|
rpcUrl: args[rpcUrlIndex + 1],
|
|
privateKey: args[privateKeyIndex + 1],
|
|
verified: args.includes("--verified")
|
|
};
|
|
|
|
// Extract Blockscout URL if verification is enabled
|
|
if (options.verified) {
|
|
const blockscoutUrlIndex = args.indexOf("--blockscout-url");
|
|
if (blockscoutUrlIndex !== -1 && blockscoutUrlIndex + 1 < args.length) {
|
|
options.blockscoutBackendUrl = args[blockscoutUrlIndex + 1];
|
|
}
|
|
}
|
|
|
|
if (!options.rpcUrl) {
|
|
console.error("Error: --rpc-url parameter is required");
|
|
process.exit(1);
|
|
}
|
|
|
|
if (options.verified && !options.blockscoutBackendUrl) {
|
|
console.error("Error: --blockscout-url parameter is required when using --verified");
|
|
process.exit(1);
|
|
}
|
|
|
|
validateDeploymentParams(options);
|
|
|
|
await buildContracts();
|
|
|
|
const deployCommand = constructDeployCommand(options);
|
|
const directEnv = options.privateKey ? { DEPLOYER_PRIVATE_KEY: options.privateKey } : undefined;
|
|
await executeDeployment(deployCommand, undefined, undefined, directEnv);
|
|
}
|