mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
feat: Add DH-AVS stagenet/testnet Hoodi deployment support (#422)
## 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>
This commit is contained in:
parent
506471db24
commit
46d752da01
23 changed files with 1346 additions and 122 deletions
|
|
@ -29,7 +29,9 @@
|
|||
"rewardsInitiator": "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955",
|
||||
"vetoCommitteeMember": "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f",
|
||||
"vetoWindowBlocks": 100,
|
||||
"validatorsStrategies": []
|
||||
"validatorsStrategies": [
|
||||
"0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0"
|
||||
]
|
||||
},
|
||||
"snowbridge": {
|
||||
"randaoCommitDelay": 4,
|
||||
|
|
@ -37,10 +39,12 @@
|
|||
"minNumRequiredSignatures": 2,
|
||||
"startBlock": 1,
|
||||
"rewardsMessageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"initialValidatorSetId": 0,
|
||||
"initialValidatorHashes": [
|
||||
"0xaeb47a269393297f4b0a3c9c9cfd00c7a4195255274cf39d83dabc2fcc9ff3d7",
|
||||
"0xf68aec7304bf37f340dae2ea20fb5271ee28a3128812b84a615da4789e458bde"
|
||||
],
|
||||
"nextValidatorSetId": 1,
|
||||
"nextValidatorHashes": [
|
||||
"0xaeb47a269393297f4b0a3c9c9cfd00c7a4195255274cf39d83dabc2fcc9ff3d7",
|
||||
"0xf68aec7304bf37f340dae2ea20fb5271ee28a3128812b84a615da4789e458bde"
|
||||
|
|
|
|||
|
|
@ -74,7 +74,19 @@
|
|||
/// The number of blocks that the Veto Committee will have to submit a veto.
|
||||
"vetoWindowBlocks": 100,
|
||||
/// The EigenLayer strategy addresses for the Validators to stake into.
|
||||
"validatorsStrategies": []
|
||||
/// The beaconChainETHStrategy is a virtual address representing native beacon chain ETH.
|
||||
/// All networks:
|
||||
/// - beaconChainETH: 0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0 (virtual, same on all networks)
|
||||
/// Hoodi testnet strategies:
|
||||
/// - stETH: 0xf8a1a66130d614c7360e868576d5e59203475fe0
|
||||
/// - WETH: 0x24579aD4fe83aC53546E5c2D3dF5F85D6383420d
|
||||
/// Mainnet strategies:
|
||||
/// - stETH: 0x93c4b944D05dfe6df7645A86cd2206016c51564D
|
||||
/// - rETH: 0x1BeE69b7dFFfA4E2d53C2a2Df135C388AD25dCD2
|
||||
/// - cbETH: 0x54945180dB7943c0ed0FEE7EdaB2Bd24620256bc
|
||||
"validatorsStrategies": [
|
||||
"0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0"
|
||||
]
|
||||
},
|
||||
"snowbridge": {
|
||||
/// Minimum delay in number of blocks that a relayer must wait between calling
|
||||
|
|
@ -86,18 +98,27 @@
|
|||
/// they desire.
|
||||
"randaoCommitExpiration": 24,
|
||||
/// The minimum number of required signatures for a Randao commit to be valid.
|
||||
/// Auto-calculated by update-beefy-checkpoint as ceil(validators * 2/3) for BFT security.
|
||||
"minNumRequiredSignatures": 2,
|
||||
/// Initial BEEFY block number.
|
||||
/// Initial BEEFY block number. Set to latest finalized block by update-beefy-checkpoint.
|
||||
/// The BeefyClient will only accept BEEFY commitments with block numbers > startBlock.
|
||||
"startBlock": 1,
|
||||
/// The origin linked to the Rewards Agent, the Agent contract who's allowed to submit
|
||||
/// new reward merkle roots to the RewardsRegistry contract.
|
||||
"rewardsMessageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
/// The BEEFY validator set ID for the initial validators.
|
||||
/// This is fetched from Beefy.ValidatorSetId on the DataHaven chain.
|
||||
"initialValidatorSetId": 0,
|
||||
/// The initial validator hashes for the BEEFY light client.
|
||||
/// Each hash is keccak256(ethereum_address) derived from the BEEFY authority public keys.
|
||||
"initialValidatorHashes": [
|
||||
"0x8626f6940e2eb28930efb4cef49b2d1f2c9c1199914b9e5506744e80bd0fd33d",
|
||||
"0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e"
|
||||
],
|
||||
/// The BEEFY validator set ID for the next validators (initialValidatorSetId + 1).
|
||||
"nextValidatorSetId": 1,
|
||||
/// The next validator hashes for the BEEFY light client.
|
||||
/// Each hash is keccak256(ethereum_address) derived from the BEEFY authority public keys.
|
||||
"nextValidatorHashes": [
|
||||
"0x8626f6940e2eb28930efb4cef49b2d1f2c9c1199914b9e5506744e80bd0fd33d",
|
||||
"0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e"
|
||||
|
|
|
|||
54
contracts/config/mainnet-ethereum.json
Normal file
54
contracts/config/mainnet-ethereum.json
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"eigenLayer": {
|
||||
"pausers": [],
|
||||
"unpauser": "0x0000000000000000000000000000000000000000",
|
||||
"rewardsUpdater": "0x0000000000000000000000000000000000000000",
|
||||
"calculationIntervalSeconds": 86400,
|
||||
"maxRewardsDuration": 6048000,
|
||||
"maxRetroactiveLength": 7776000,
|
||||
"maxFutureLength": 2592000,
|
||||
"genesisRewardsTimestamp": 1710979200,
|
||||
"activationDelay": 7200,
|
||||
"globalCommissionBips": 1000,
|
||||
"executorMultisig": "0x0000000000000000000000000000000000000000",
|
||||
"operationsMultisig": "0x0000000000000000000000000000000000000000",
|
||||
"minWithdrawalDelayBlocks": 50400,
|
||||
"delegationInitPausedStatus": 0,
|
||||
"eigenPodManagerInitPausedStatus": 0,
|
||||
"rewardsCoordinatorInitPausedStatus": 0,
|
||||
"allocationManagerInitPausedStatus": 0,
|
||||
"deallocationDelay": 100800,
|
||||
"allocationConfigurationDelay": 1200,
|
||||
"beaconChainGenesisTimestamp": 1606824023,
|
||||
"delegationManager": "0x39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A",
|
||||
"strategyManager": "0x858646372CC42E1A627fcE94aa7A7033e7CF075A",
|
||||
"eigenPodManager": "0x91E677b07F7AF907ec9a428aafA9fc14a0d3A338",
|
||||
"avsDirectory": "0x135dda560e946695d6f155dacafc6f1f25c1f5af",
|
||||
"rewardsCoordinator": "0x7750d328b314EfFa365A0402CcfD489B80B0adda",
|
||||
"allocationManager": "0x948a420b8CC1d6BFd0B6087C2E7c344a2CD0bc39",
|
||||
"permissionController": "0x25E5F8B1E7aDf44518d35D5B2271f114e081f0E5"
|
||||
},
|
||||
"avs": {
|
||||
"avsOwner": "0x0000000000000000000000000000000000000000",
|
||||
"rewardsInitiator": "0x0000000000000000000000000000000000000000",
|
||||
"vetoCommitteeMember": "0x0000000000000000000000000000000000000000",
|
||||
"vetoWindowBlocks": 7200,
|
||||
"validatorsStrategies": [
|
||||
"0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0",
|
||||
"0x93c4b944D05dfe6df7645A86cd2206016c51564D",
|
||||
"0x1BeE69b7dFFfA4E2d53C2a2Df135C388AD25dCD2",
|
||||
"0x54945180dB7943c0ed0FEE7EdaB2Bd24620256bc"
|
||||
]
|
||||
},
|
||||
"snowbridge": {
|
||||
"randaoCommitDelay": 4,
|
||||
"randaoCommitExpiration": 24,
|
||||
"minNumRequiredSignatures": 16,
|
||||
"startBlock": 1,
|
||||
"rewardsMessageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"initialValidatorSetId": 0,
|
||||
"initialValidatorHashes": [],
|
||||
"nextValidatorSetId": 1,
|
||||
"nextValidatorHashes": []
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
"eigenPodManager": "0xcd1442415Fc5C29Aa848A49d2e232720BE07976c",
|
||||
"avsDirectory": "0xD58f6844f79eB1fbd9f7091d05f7cb30d3363926",
|
||||
"rewardsCoordinator": "0x29e8572678e0c272350aa0b4B8f304E47EBcd5e7",
|
||||
"allocationManager": "0x95a7431400F362F3647a69535C5666cA0133CAA0",
|
||||
"allocationManager": "0x95a7431400F362F3647a69535C5666cA0133CAA0",
|
||||
"permissionController": "0xdcCF401fD121d8C542E96BC1d0078884422aFAD2"
|
||||
},
|
||||
"avs": {
|
||||
|
|
@ -35,21 +35,31 @@
|
|||
"rewardsInitiator": "0xe30a38ac89ffE5A86D5389Bfbf70C7EC766FbB6e",
|
||||
"vetoCommitteeMember": "0xe30a38ac89ffE5A86D5389Bfbf70C7EC766FbB6e",
|
||||
"vetoWindowBlocks": 100,
|
||||
"validatorsStrategies": []
|
||||
"validatorsStrategies": [
|
||||
"0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0",
|
||||
"0xf8a1a66130d614c7360e868576d5e59203475fe0",
|
||||
"0x24579aD4fe83aC53546E5c2D3dF5F85D6383420d"
|
||||
]
|
||||
},
|
||||
"snowbridge": {
|
||||
"randaoCommitDelay": 4,
|
||||
"randaoCommitExpiration": 24,
|
||||
"minNumRequiredSignatures": 2,
|
||||
"startBlock": 1,
|
||||
"rewardsMessageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"minNumRequiredSignatures": 3,
|
||||
"startBlock": 1299215,
|
||||
"rewardsMessageOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd",
|
||||
"initialValidatorSetId": 2179,
|
||||
"initialValidatorHashes": [
|
||||
"0xaeb47a269393297f4b0a3c9c9cfd00c7a4195255274cf39d83dabc2fcc9ff3d7",
|
||||
"0xf68aec7304bf37f340dae2ea20fb5271ee28a3128812b84a615da4789e458bde"
|
||||
"0x07ce4f2cd558f4d4b529a3362b6ff7d616ca0893b53252dc62829b8218ea5c10",
|
||||
"0xaea5344f086d3be7c94cf3a47436bcbb98de23cf1ee773a9180cfecab0453a50",
|
||||
"0xcd3a33755b27fe810dfb780b3f1df1c25efa1bb826ca618e41022fa900876087",
|
||||
"0x4f4ce8cad711a4b33d15095091f8a98eaf9bfd1b39a9159e605cf5d6783cc667"
|
||||
],
|
||||
"nextValidatorSetId": 2180,
|
||||
"nextValidatorHashes": [
|
||||
"0xaeb47a269393297f4b0a3c9c9cfd00c7a4195255274cf39d83dabc2fcc9ff3d7",
|
||||
"0xf68aec7304bf37f340dae2ea20fb5271ee28a3128812b84a615da4789e458bde"
|
||||
"0x07ce4f2cd558f4d4b529a3362b6ff7d616ca0893b53252dc62829b8218ea5c10",
|
||||
"0xaea5344f086d3be7c94cf3a47436bcbb98de23cf1ee773a9180cfecab0453a50",
|
||||
"0xcd3a33755b27fe810dfb780b3f1df1c25efa1bb826ca618e41022fa900876087",
|
||||
"0x4f4ce8cad711a4b33d15095091f8a98eaf9bfd1b39a9159e605cf5d6783cc667"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
71
contracts/config/testnet-hoodi.json
Normal file
71
contracts/config/testnet-hoodi.json
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
"eigenLayer": {
|
||||
"pausers": [
|
||||
"0x64D78399B0fa32EA72959f33edCF313159F3c13D"
|
||||
],
|
||||
"unpauser": "0xE3328cb5068924119d6170496c4AB2dD12c12d15",
|
||||
"rewardsUpdater": "0xe30a38ac89ffE5A86D5389Bfbf70C7EC766FbB6e",
|
||||
"calculationIntervalSeconds": 86400,
|
||||
"maxRewardsDuration": 6048000,
|
||||
"maxRetroactiveLength": 7776000,
|
||||
"maxFutureLength": 2592000,
|
||||
"genesisRewardsTimestamp": 1710979200,
|
||||
"activationDelay": 7200,
|
||||
"globalCommissionBips": 1000,
|
||||
"executorMultisig": "0xE3328cb5068924119d6170496c4AB2dD12c12d15",
|
||||
"operationsMultisig": "0xE7f4E30D2619273468afe9EC0Acf805E55532257",
|
||||
"minWithdrawalDelayBlocks": 50,
|
||||
"delegationInitPausedStatus": 0,
|
||||
"eigenPodManagerInitPausedStatus": 0,
|
||||
"rewardsCoordinatorInitPausedStatus": 0,
|
||||
"allocationManagerInitPausedStatus": 0,
|
||||
"deallocationDelay": 50,
|
||||
"allocationConfigurationDelay": 75,
|
||||
"beaconChainGenesisTimestamp": 1710666600,
|
||||
"delegationManager": "0x867837a9722C512e0862d8c2E15b8bE220E8b87d",
|
||||
"strategyManager": "0xeE45e76ddbEDdA2918b8C7E3035cd37Eab3b5D41",
|
||||
"eigenPodManager": "0xcd1442415Fc5C29Aa848A49d2e232720BE07976c",
|
||||
"avsDirectory": "0xD58f6844f79eB1fbd9f7091d05f7cb30d3363926",
|
||||
"rewardsCoordinator": "0x29e8572678e0c272350aa0b4B8f304E47EBcd5e7",
|
||||
"allocationManager": "0x95a7431400F362F3647a69535C5666cA0133CAA0",
|
||||
"permissionController": "0xdcCF401fD121d8C542E96BC1d0078884422aFAD2"
|
||||
},
|
||||
"avs": {
|
||||
"avsOwner": "0x0000000000000000000000000000000000000000",
|
||||
"rewardsInitiator": "0x0000000000000000000000000000000000000000",
|
||||
"vetoCommitteeMember": "0x0000000000000000000000000000000000000000",
|
||||
"vetoWindowBlocks": 100,
|
||||
"validatorsStrategies": [
|
||||
"0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0",
|
||||
"0xf8a1a66130d614c7360e868576d5e59203475fe0",
|
||||
"0x24579aD4fe83aC53546E5c2D3dF5F85D6383420d"
|
||||
]
|
||||
},
|
||||
"snowbridge": {
|
||||
"randaoCommitDelay": 4,
|
||||
"randaoCommitExpiration": 24,
|
||||
"minNumRequiredSignatures": 5,
|
||||
"startBlock": 1381173,
|
||||
"rewardsMessageOrigin": "0xd0d6dbd1ffb401ef613f00e93cd5061ecec03ae35d2f820cd6754a5b5f020215",
|
||||
"initialValidatorSetId": 2303,
|
||||
"initialValidatorHashes": [
|
||||
"0x0ec3102f334aba804c18b843e45ec874005587122a1b49273b1b04d6fd98b1a2",
|
||||
"0xb277ac8a7aafc125d7da813a9b0fdae144922c165bfae194ebd94d6f3e4fe68e",
|
||||
"0xcc6aefdce3f83d204893cb57388a72b4553b613f95b437ce548582470e62d1e7",
|
||||
"0xcefa9940d25d21ac6d0a579727e8812283ed00d7ace9dfc9e30a0f95a0ea7bdd",
|
||||
"0x8e720aed537cb30d204f1de9fb5aab6e0129acfc3f41a3c69259231c1f0f2685",
|
||||
"0x5f883131cf6667cb8c2279caa182298e174bbca35c7c8c5679df6bad73be85cf",
|
||||
"0x744ae85e99103a5ebcf9dd2fdcc18743012f0336f497fd3a05243a26a1a031b7"
|
||||
],
|
||||
"nextValidatorSetId": 2304,
|
||||
"nextValidatorHashes": [
|
||||
"0x0ec3102f334aba804c18b843e45ec874005587122a1b49273b1b04d6fd98b1a2",
|
||||
"0xb277ac8a7aafc125d7da813a9b0fdae144922c165bfae194ebd94d6f3e4fe68e",
|
||||
"0xcc6aefdce3f83d204893cb57388a72b4553b613f95b437ce548582470e62d1e7",
|
||||
"0xcefa9940d25d21ac6d0a579727e8812283ed00d7ace9dfc9e30a0f95a0ea7bdd",
|
||||
"0x8e720aed537cb30d204f1de9fb5aab6e0129acfc3f41a3c69259231c1f0f2685",
|
||||
"0x5f883131cf6667cb8c2279caa182298e174bbca35c7c8c5679df6bad73be85cf",
|
||||
"0x744ae85e99103a5ebcf9dd2fdcc18743012f0336f497fd3a05243a26a1a031b7"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,9 @@ contract Config {
|
|||
uint256 randaoCommitExpiration;
|
||||
uint256 minNumRequiredSignatures;
|
||||
uint64 startBlock;
|
||||
uint128 initialValidatorSetId;
|
||||
bytes32[] initialValidatorHashes;
|
||||
uint128 nextValidatorSetId;
|
||||
bytes32[] nextValidatorHashes;
|
||||
bytes32 rewardsMessageOrigin;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -208,10 +208,12 @@ abstract contract DeployBase is Script, DeployParams, Accounts {
|
|||
SnowbridgeConfig memory config
|
||||
) internal returns (BeefyClient) {
|
||||
// Create validator sets using the MerkleUtils library
|
||||
BeefyClient.ValidatorSet memory validatorSet =
|
||||
ValidatorsUtils._buildValidatorSet(0, config.initialValidatorHashes);
|
||||
BeefyClient.ValidatorSet memory nextValidatorSet =
|
||||
ValidatorsUtils._buildValidatorSet(1, config.nextValidatorHashes);
|
||||
BeefyClient.ValidatorSet memory validatorSet = ValidatorsUtils._buildValidatorSet(
|
||||
config.initialValidatorSetId, config.initialValidatorHashes
|
||||
);
|
||||
BeefyClient.ValidatorSet memory nextValidatorSet = ValidatorsUtils._buildValidatorSet(
|
||||
config.nextValidatorSetId, config.nextValidatorHashes
|
||||
);
|
||||
|
||||
// Deploy BeefyClient
|
||||
vm.broadcast(_deployerPrivateKey);
|
||||
|
|
|
|||
|
|
@ -29,10 +29,11 @@ import {
|
|||
} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
|
||||
|
||||
/**
|
||||
* @title DeployTestnet
|
||||
* @notice Deployment script for testnets (hoodi) - references existing EigenLayer contracts
|
||||
* @title DeployLive
|
||||
* @notice Deployment script for live networks (hoodi testnet, ethereum mainnet)
|
||||
* @dev References existing EigenLayer contracts on the target chain
|
||||
*/
|
||||
contract DeployTestnet is DeployBase {
|
||||
contract DeployLive is DeployBase {
|
||||
string public networkName;
|
||||
|
||||
function run() public {
|
||||
|
|
@ -40,7 +41,7 @@ contract DeployTestnet is DeployBase {
|
|||
networkName = vm.envString("NETWORK");
|
||||
require(
|
||||
bytes(networkName).length > 0,
|
||||
"NETWORK environment variable required for testnet deployment"
|
||||
"NETWORK environment variable required for live deployment"
|
||||
);
|
||||
|
||||
_validateNetwork(networkName);
|
||||
|
|
@ -49,7 +50,7 @@ contract DeployTestnet is DeployBase {
|
|||
address avsOwnerEnv = vm.envOr("AVS_OWNER_ADDRESS", address(0));
|
||||
require(
|
||||
avsOwnerEnv != address(0),
|
||||
"AVS_OWNER_ADDRESS env variable required for testnet deployments"
|
||||
"AVS_OWNER_ADDRESS env variable required for live deployments"
|
||||
);
|
||||
|
||||
_executeSharedDeployment();
|
||||
|
|
@ -60,8 +61,8 @@ contract DeployTestnet is DeployBase {
|
|||
return networkName;
|
||||
}
|
||||
|
||||
function _getDeploymentMode() internal pure override returns (string memory) {
|
||||
return "HOODI_TESTNET";
|
||||
function _getDeploymentMode() internal view override returns (string memory) {
|
||||
return string.concat("LIVE_", networkName);
|
||||
}
|
||||
|
||||
function _setupEigenLayerContracts(
|
||||
|
|
@ -100,16 +101,16 @@ contract DeployTestnet is DeployBase {
|
|||
Logging.logStep("All EigenLayer contracts referenced successfully");
|
||||
Logging.logFooter();
|
||||
|
||||
// Testnet deployments create their own ProxyAdmin (no existing one from EigenLayer deployment)
|
||||
// Live deployments create their own ProxyAdmin (no existing one from EigenLayer deployment)
|
||||
return ProxyAdmin(address(0)); // Will be created in _createServiceManagerProxy
|
||||
}
|
||||
|
||||
function _createServiceManagerProxy(
|
||||
DataHavenServiceManager implementation,
|
||||
ProxyAdmin, // Ignored for testnet deployment
|
||||
ProxyAdmin, // Ignored for live deployment
|
||||
ServiceManagerInitParams memory params
|
||||
) internal override returns (DataHavenServiceManager) {
|
||||
// Testnet deployment creates its own ProxyAdmin for the service manager
|
||||
// Live deployment creates its own ProxyAdmin for the service manager
|
||||
vm.broadcast(_deployerPrivateKey);
|
||||
ProxyAdmin proxyAdmin = new ProxyAdmin();
|
||||
Logging.logContractDeployed("ProxyAdmin", address(proxyAdmin));
|
||||
|
|
@ -186,7 +187,7 @@ contract DeployTestnet is DeployBase {
|
|||
);
|
||||
json = string.concat(json, '"RewardsAgent": "', vm.toString(rewardsAgent), '",');
|
||||
|
||||
// EigenLayer contracts (existing on testnet)
|
||||
// EigenLayer contracts (existing on live network)
|
||||
json = string.concat(json, '"DelegationManager": "', vm.toString(address(delegation)), '",');
|
||||
json = string.concat(
|
||||
json, '"StrategyManager": "', vm.toString(address(strategyManager)), '",'
|
||||
|
|
@ -209,23 +210,34 @@ contract DeployTestnet is DeployBase {
|
|||
Logging.logInfo(string.concat("Deployment info saved to: ", deploymentPath));
|
||||
}
|
||||
|
||||
// TESTNET-SPECIFIC FUNCTIONS
|
||||
// LIVE DEPLOYMENT FUNCTIONS
|
||||
|
||||
/**
|
||||
* @notice Validate that the network is hoodi (the only supported testnet)
|
||||
* @notice Validate that the network is in the supported allowlist
|
||||
* @dev Supported networks:
|
||||
* - "hoodi", "stagenet-hoodi", "testnet-hoodi" (Hoodi testnet)
|
||||
* - "ethereum", "mainnet-ethereum" (Ethereum mainnet)
|
||||
*/
|
||||
function _validateNetwork(
|
||||
string memory network
|
||||
) internal pure {
|
||||
bytes32 networkHash = keccak256(abi.encodePacked(network));
|
||||
bytes32 h = keccak256(bytes(network));
|
||||
|
||||
if (networkHash != keccak256(abi.encodePacked("hoodi"))) {
|
||||
revert(
|
||||
string.concat(
|
||||
"Unsupported testnet network: ", network, ". Supported networks: hoodi"
|
||||
)
|
||||
);
|
||||
if (
|
||||
h == keccak256("hoodi") || h == keccak256("stagenet-hoodi")
|
||||
|| h == keccak256("testnet-hoodi") || h == keccak256("mainnet-ethereum")
|
||||
|| h == keccak256("ethereum")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
revert(
|
||||
string.concat(
|
||||
"Unsupported network: ",
|
||||
network,
|
||||
". Supported: hoodi, stagenet-hoodi, testnet-hoodi, ethereum, mainnet-ethereum"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -31,9 +31,27 @@ contract DeployParams is Script, Config {
|
|||
bool isDevMode = keccak256(abi.encodePacked(vm.envOr("DEV_MODE", string("false"))))
|
||||
== keccak256(abi.encodePacked("true"));
|
||||
if (isDevMode) {
|
||||
config.initialValidatorSetId = 0;
|
||||
config.initialValidatorHashes = TestUtils.generateMockValidators(10);
|
||||
config.nextValidatorSetId = 1;
|
||||
config.nextValidatorHashes = TestUtils.generateMockValidators(10);
|
||||
} else {
|
||||
// Load validator set IDs (default to 0/1 for backwards compatibility)
|
||||
try vm.parseJsonUint(configJson, ".snowbridge.initialValidatorSetId") returns (
|
||||
uint256 val
|
||||
) {
|
||||
config.initialValidatorSetId = uint128(val);
|
||||
} catch {
|
||||
config.initialValidatorSetId = 0;
|
||||
}
|
||||
try vm.parseJsonUint(configJson, ".snowbridge.nextValidatorSetId") returns (
|
||||
uint256 val
|
||||
) {
|
||||
config.nextValidatorSetId = uint128(val);
|
||||
} catch {
|
||||
config.nextValidatorSetId = config.initialValidatorSetId + 1;
|
||||
}
|
||||
|
||||
config.initialValidatorHashes =
|
||||
_loadValidatorsFromConfig(configJson, ".snowbridge.initialValidatorHashes");
|
||||
config.nextValidatorHashes =
|
||||
|
|
|
|||
|
|
@ -1038,8 +1038,8 @@ impl pallet_evm_chain_id::Config for Runtime {}
|
|||
|
||||
// --- Snowbridge Config Constants & Parameter Types ---
|
||||
parameter_types! {
|
||||
// Hoodi testnet genesis hash
|
||||
pub const StagenetGenesisHash: [u8; 32] = hex_literal::hex!("bbe312868b376a3001692a646dd2d7d1e4406380dfd86b98aa8a34d1557c971b");
|
||||
// DataHaven stagenet genesis hash
|
||||
pub const StagenetGenesisHash: [u8; 32] = hex_literal::hex!("72d0856fd339e09cb21df7bac8ac3120bd871e327ec0e1658395df68acef9bee");
|
||||
pub UniversalLocation: InteriorLocation = [
|
||||
GlobalConsensus(ByGenesis(StagenetGenesisHash::get()))
|
||||
].into();
|
||||
|
|
@ -1928,4 +1928,95 @@ mod tests {
|
|||
assert!(result.is_ok(), "Message from authorized origin should be accepted");
|
||||
});
|
||||
}
|
||||
|
||||
/// Test that the ExternalValidatorRewardsAccount is correctly derived from the pallet ID.
|
||||
///
|
||||
/// This verifies that `PalletId(*b"dh/evrew").into_account_truncating()` produces the
|
||||
/// expected AccountId20 value, which is used in the Rewards Agent ID computation.
|
||||
#[test]
|
||||
fn test_external_validator_rewards_account_derivation() {
|
||||
// Expected account: "modl" (4 bytes) + "dh/evrew" (8 bytes) + zeros (8 bytes) = 20 bytes
|
||||
// "modl" = 0x6d6f646c
|
||||
// "dh/evrew" = 0x64682f6576726577
|
||||
// Result = 0x6d6f646c64682f65767265770000000000000000
|
||||
let expected_account = AccountId::from(hex_literal::hex!(
|
||||
"6d6f646c64682f65767265770000000000000000"
|
||||
));
|
||||
|
||||
let actual_account = ExternalValidatorRewardsAccount::get();
|
||||
|
||||
assert_eq!(
|
||||
actual_account, expected_account,
|
||||
"ExternalValidatorRewardsAccount must be derived correctly from PalletId 'dh/evrew'"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that the Rewards Agent ID (used for Snowbridge outbound messages from the rewards pallet)
|
||||
/// is correctly computed from the chain's genesis hash and the ExternalValidatorRewardsAccount.
|
||||
///
|
||||
/// This test verifies the value that should be set as `RewardsAgentOrigin` in runtime parameters
|
||||
/// and as `rewardsMessageOrigin` in the AVS contract configuration.
|
||||
///
|
||||
/// The Agent ID is computed following Snowbridge's pattern for GlobalConsensus locations:
|
||||
/// blake2_256(SCALE_ENCODE("GlobalConsensus", ByGenesis(genesis_hash), compact_len, "AccountKey20", account_key))
|
||||
///
|
||||
/// Note: Standard `AgentIdOf` doesn't support direct AccountKey20 without a Parachain junction,
|
||||
/// so we compute the hash directly here.
|
||||
#[test]
|
||||
fn test_rewards_agent_id_computation() {
|
||||
use codec::Encode;
|
||||
use sp_core::H256;
|
||||
use sp_io::hashing::blake2_256;
|
||||
use xcm::prelude::NetworkId;
|
||||
|
||||
// Use the StagenetGenesisHash parameter
|
||||
let genesis_hash: [u8; 32] = StagenetGenesisHash::get();
|
||||
|
||||
// Get the rewards pallet account (derived from PalletId "dh/evrew")
|
||||
let rewards_account: [u8; 20] = ExternalValidatorRewardsAccount::get().into();
|
||||
|
||||
// Build the location description following Snowbridge's encoding pattern:
|
||||
// ("GlobalConsensus", ByGenesis(genesis_hash), compact_len(interior), "AccountKey20", account_key)
|
||||
//
|
||||
// This matches the pattern in snowbridge_core::location::DescribeGlobalPrefix
|
||||
// combined with DescribeTokenTerminal for AccountKey20.
|
||||
|
||||
// Interior description: "AccountKey20" + account_key (no length prefix for fixed arrays)
|
||||
let interior: Vec<u8> = (b"AccountKey20", rewards_account).encode();
|
||||
|
||||
// Full encoding: "GlobalConsensus" + NetworkId::ByGenesis(genesis) + interior
|
||||
let encoded: Vec<u8> = (
|
||||
b"GlobalConsensus",
|
||||
NetworkId::ByGenesis(genesis_hash),
|
||||
interior,
|
||||
)
|
||||
.encode();
|
||||
|
||||
// Hash with blake2_256
|
||||
let computed_agent_id = H256(blake2_256(&encoded));
|
||||
|
||||
// Expected Agent ID - this value must match RewardsAgentOrigin in runtime_params.rs
|
||||
// If this test fails, update RewardsAgentOrigin to match the computed value.
|
||||
let expected_agent_id = H256(hex_literal::hex!(
|
||||
"56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd"
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
computed_agent_id,
|
||||
expected_agent_id,
|
||||
"Computed Rewards Agent ID must match expected value.\n\
|
||||
This value should be set as:\n\
|
||||
- RewardsAgentOrigin in runtime_params.rs\n\
|
||||
- rewardsMessageOrigin in AVS contract config\n\
|
||||
\n\
|
||||
Rewards account: 0x{}\n\
|
||||
Genesis hash: 0x{}\n\
|
||||
Computed: {:?}\n\
|
||||
Expected: {:?}",
|
||||
hex::encode(rewards_account),
|
||||
hex::encode(genesis_hash),
|
||||
computed_agent_id,
|
||||
expected_agent_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,10 +51,14 @@ pub mod dynamic_params {
|
|||
|
||||
#[codec(index = 3)]
|
||||
#[allow(non_upper_case_globals)]
|
||||
/// The RewardsAgentOrigin is the origin of the rewards agent, which is its ID.
|
||||
/// TODO: Decide which agent origin we want to use. Currently for testing it's the zero hash
|
||||
/// The RewardsAgentOrigin is the Agent ID for the rewards pallet's outbound Snowbridge messages.
|
||||
/// Computed as: blake2_256(SCALE_ENCODE("GlobalConsensus", ByGenesis(genesis_hash), interior))
|
||||
/// where interior = SCALE_ENCODE("AccountKey20", ExternalValidatorRewardsAccount)
|
||||
///
|
||||
/// For stagenet with genesis hash 0x72d0856fd339e09cb21df7bac8ac3120bd871e327ec0e1658395df68acef9bee
|
||||
/// and rewards account 0x6d6f646c64682f65767265770000000000000000 (from PalletId "dh/evrew"):
|
||||
pub static RewardsAgentOrigin: H256 = H256::from_slice(&hex!(
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
"56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd"
|
||||
));
|
||||
|
||||
// Proportion of fees allocated to the Treasury (remainder are burned).
|
||||
|
|
|
|||
|
|
@ -1041,8 +1041,8 @@ impl pallet_evm_chain_id::Config for Runtime {}
|
|||
|
||||
// --- Snowbridge Config Constants & Parameter Types ---
|
||||
parameter_types! {
|
||||
// Hoodi testnet genesis hash
|
||||
pub const TestnetGenesisHash: [u8; 32] = hex_literal::hex!("bbe312868b376a3001692a646dd2d7d1e4406380dfd86b98aa8a34d1557c971b");
|
||||
// DataHaven testnet genesis hash
|
||||
pub const TestnetGenesisHash: [u8; 32] = hex_literal::hex!("dbf403d348916fb0694485bc7f9c0d8c53fdf86664ebac019af209c090c3df99");
|
||||
pub UniversalLocation: InteriorLocation = [
|
||||
GlobalConsensus(ByGenesis(TestnetGenesisHash::get()))
|
||||
].into();
|
||||
|
|
@ -1950,4 +1950,95 @@ mod tests {
|
|||
assert!(result.is_ok(), "Message from authorized origin should be accepted");
|
||||
});
|
||||
}
|
||||
|
||||
/// Test that the ExternalValidatorRewardsAccount is correctly derived from the pallet ID.
|
||||
///
|
||||
/// This verifies that `PalletId(*b"dh/evrew").into_account_truncating()` produces the
|
||||
/// expected AccountId20 value, which is used in the Rewards Agent ID computation.
|
||||
#[test]
|
||||
fn test_external_validator_rewards_account_derivation() {
|
||||
// Expected account: "modl" (4 bytes) + "dh/evrew" (8 bytes) + zeros (8 bytes) = 20 bytes
|
||||
// "modl" = 0x6d6f646c
|
||||
// "dh/evrew" = 0x64682f6576726577
|
||||
// Result = 0x6d6f646c64682f65767265770000000000000000
|
||||
let expected_account = AccountId::from(hex_literal::hex!(
|
||||
"6d6f646c64682f65767265770000000000000000"
|
||||
));
|
||||
|
||||
let actual_account = ExternalValidatorRewardsAccount::get();
|
||||
|
||||
assert_eq!(
|
||||
actual_account, expected_account,
|
||||
"ExternalValidatorRewardsAccount must be derived correctly from PalletId 'dh/evrew'"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that the Rewards Agent ID (used for Snowbridge outbound messages from the rewards pallet)
|
||||
/// is correctly computed from the chain's genesis hash and the ExternalValidatorRewardsAccount.
|
||||
///
|
||||
/// This test verifies the value that should be set as `RewardsAgentOrigin` in runtime parameters
|
||||
/// and as `rewardsMessageOrigin` in the AVS contract configuration.
|
||||
///
|
||||
/// The Agent ID is computed following Snowbridge's pattern for GlobalConsensus locations:
|
||||
/// blake2_256(SCALE_ENCODE("GlobalConsensus", ByGenesis(genesis_hash), compact_len, "AccountKey20", account_key))
|
||||
///
|
||||
/// Note: Standard `AgentIdOf` doesn't support direct AccountKey20 without a Parachain junction,
|
||||
/// so we compute the hash directly here.
|
||||
#[test]
|
||||
fn test_rewards_agent_id_computation() {
|
||||
use codec::Encode;
|
||||
use sp_core::H256;
|
||||
use sp_io::hashing::blake2_256;
|
||||
use xcm::prelude::NetworkId;
|
||||
|
||||
// Use the TestnetGenesisHash parameter
|
||||
let genesis_hash: [u8; 32] = TestnetGenesisHash::get();
|
||||
|
||||
// Get the rewards pallet account (derived from PalletId "dh/evrew")
|
||||
let rewards_account: [u8; 20] = ExternalValidatorRewardsAccount::get().into();
|
||||
|
||||
// Build the location description following Snowbridge's encoding pattern:
|
||||
// ("GlobalConsensus", ByGenesis(genesis_hash), compact_len(interior), "AccountKey20", account_key)
|
||||
//
|
||||
// This matches the pattern in snowbridge_core::location::DescribeGlobalPrefix
|
||||
// combined with DescribeTokenTerminal for AccountKey20.
|
||||
|
||||
// Interior description: "AccountKey20" + account_key (no length prefix for fixed arrays)
|
||||
let interior: Vec<u8> = (b"AccountKey20", rewards_account).encode();
|
||||
|
||||
// Full encoding: "GlobalConsensus" + NetworkId::ByGenesis(genesis) + interior
|
||||
let encoded: Vec<u8> = (
|
||||
b"GlobalConsensus",
|
||||
NetworkId::ByGenesis(genesis_hash),
|
||||
interior,
|
||||
)
|
||||
.encode();
|
||||
|
||||
// Hash with blake2_256
|
||||
let computed_agent_id = H256(blake2_256(&encoded));
|
||||
|
||||
// Expected Agent ID - this value must match RewardsAgentOrigin in runtime_params.rs
|
||||
// If this test fails, update RewardsAgentOrigin to match the computed value.
|
||||
let expected_agent_id = H256(hex_literal::hex!(
|
||||
"d0d6dbd1ffb401ef613f00e93cd5061ecec03ae35d2f820cd6754a5b5f020215"
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
computed_agent_id,
|
||||
expected_agent_id,
|
||||
"Computed Rewards Agent ID must match expected value.\n\
|
||||
This value should be set as:\n\
|
||||
- RewardsAgentOrigin in runtime_params.rs\n\
|
||||
- rewardsMessageOrigin in AVS contract config\n\
|
||||
\n\
|
||||
Rewards account: 0x{}\n\
|
||||
Genesis hash: 0x{}\n\
|
||||
Computed: {:?}\n\
|
||||
Expected: {:?}",
|
||||
hex::encode(rewards_account),
|
||||
hex::encode(genesis_hash),
|
||||
computed_agent_id,
|
||||
expected_agent_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,10 +49,14 @@ pub mod dynamic_params {
|
|||
|
||||
#[codec(index = 3)]
|
||||
#[allow(non_upper_case_globals)]
|
||||
/// The RewardsAgentOrigin is the hash of the string "external_validators_rewards"
|
||||
/// TODO: Decide which agent origin we want to use. Currently for testing it's the zero hash
|
||||
/// The RewardsAgentOrigin is the Agent ID for the rewards pallet's outbound Snowbridge messages.
|
||||
/// Computed as: blake2_256(SCALE_ENCODE("GlobalConsensus", ByGenesis(genesis_hash), interior))
|
||||
/// where interior = SCALE_ENCODE("AccountKey20", ExternalValidatorRewardsAccount)
|
||||
///
|
||||
/// For testnet with genesis hash 0xdbf403d348916fb0694485bc7f9c0d8c53fdf86664ebac019af209c090c3df99
|
||||
/// and rewards account 0x6d6f646c64682f65767265770000000000000000 (from PalletId "dh/evrew"):
|
||||
pub static RewardsAgentOrigin: H256 = H256::from_slice(&hex!(
|
||||
"c505dfb2df107d106d08bd0f1a0acd10052ca9aa078629a4ccfd0c90c6e69b65"
|
||||
"d0d6dbd1ffb401ef613f00e93cd5061ecec03ae35d2f820cd6754a5b5f020215"
|
||||
));
|
||||
|
||||
// Proportion of fees allocated to the Treasury (remainder are burned).
|
||||
|
|
|
|||
305
test/cli/handlers/contracts/beefy-checkpoint.ts
Normal file
305
test/cli/handlers/contracts/beefy-checkpoint.ts
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
import invariant from "tiny-invariant";
|
||||
import { logger, printDivider, printHeader } from "utils";
|
||||
import { createPapiConnectors } from "utils/papi";
|
||||
import { type Hex, keccak256 } from "viem";
|
||||
import { buildNetworkId } from "../../../configs/contracts/config";
|
||||
import { compressedPubKeyToEthereumAddress } from "../../../launcher/datahaven";
|
||||
|
||||
interface UpdateBeefyCheckpointOptions {
|
||||
chain: string;
|
||||
environment?: string;
|
||||
rpcUrl: string;
|
||||
}
|
||||
|
||||
interface BeefyCheckpointData {
|
||||
startBlock: number;
|
||||
minNumRequiredSignatures: number;
|
||||
initialValidatorSetId: number;
|
||||
initialValidatorHashes: string[];
|
||||
nextValidatorSetId: number;
|
||||
nextValidatorHashes: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array of compressed public keys to authority hashes.
|
||||
*
|
||||
* @param authorityPublicKeys - Array of compressed public keys as hex strings
|
||||
* @returns Array of authority hashes (keccak256 of Ethereum addresses)
|
||||
*/
|
||||
const computeAuthorityHashes = (authorityPublicKeys: string[]): string[] => {
|
||||
const authorityHashes: string[] = [];
|
||||
for (const compressedKey of authorityPublicKeys) {
|
||||
const ethAddress = compressedPubKeyToEthereumAddress(compressedKey);
|
||||
const authorityHash = keccak256(ethAddress as Hex);
|
||||
authorityHashes.push(authorityHash);
|
||||
logger.debug(
|
||||
` ${compressedKey.slice(0, 20)}... -> ${ethAddress} -> ${authorityHash.slice(0, 20)}...`
|
||||
);
|
||||
}
|
||||
return authorityHashes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the minimum number of required signatures for BFT security.
|
||||
* Uses the same formula as Snowbridge's BeefyClient contract to ensure
|
||||
* strictly more than 2/3 of validators must sign.
|
||||
*
|
||||
* Formula: n - floor((n-1)/3)
|
||||
*
|
||||
* This ensures strictly > 2/3 majority. For example:
|
||||
* - n=3: returns 3 (not 2, which would be exactly 2/3)
|
||||
* - n=6: returns 5 (not 4, which would be exactly 2/3)
|
||||
* - n=100: returns 67 (strictly > 66.67)
|
||||
*
|
||||
* @see https://github.com/datahaven-xyz/snowbridge/blob/main/contracts/src/BeefyClient.sol
|
||||
* @param validatorCount - The number of validators
|
||||
* @returns The minimum number of required signatures
|
||||
*/
|
||||
const calculateMinRequiredSignatures = (validatorCount: number): number => {
|
||||
// For BFT security, we need strictly > 2/3 of validators to sign
|
||||
// This matches Snowbridge's computeQuorum function
|
||||
if (validatorCount <= 3) {
|
||||
return validatorCount;
|
||||
}
|
||||
return validatorCount - Math.floor((validatorCount - 1) / 3);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches BEEFY checkpoint data from a DataHaven chain including both current and next
|
||||
* authority sets along with their validator set IDs, the latest finalized block,
|
||||
* and calculates the minimum required signatures.
|
||||
*
|
||||
* All queries are performed at the same finalized block to ensure consistency.
|
||||
*
|
||||
* @param rpcUrl - WebSocket RPC endpoint of the DataHaven chain
|
||||
* @returns BEEFY checkpoint data with validator set IDs, authority hashes, startBlock, and minNumRequiredSignatures
|
||||
*/
|
||||
const fetchBeefyCheckpointData = async (rpcUrl: string): Promise<BeefyCheckpointData> => {
|
||||
logger.info(`📡 Connecting to DataHaven chain at ${rpcUrl}...`);
|
||||
|
||||
const { client: papiClient, typedApi: dhApi } = createPapiConnectors(rpcUrl);
|
||||
|
||||
try {
|
||||
// First, get the finalized block hash to use for all subsequent queries
|
||||
logger.info("🔍 Fetching latest finalized block...");
|
||||
const finalizedBlock = await papiClient.getFinalizedBlock();
|
||||
const startBlock = finalizedBlock.number;
|
||||
const blockHash = finalizedBlock.hash;
|
||||
logger.success(`Latest finalized block: ${startBlock} (${blockHash})`);
|
||||
|
||||
// Fetch all BEEFY data in parallel at the same finalized block
|
||||
logger.info("🔍 Fetching BEEFY data (ValidatorSetId, Authorities, NextAuthorities)...");
|
||||
const [validatorSetId, authoritiesRaw, nextAuthoritiesRaw] = await Promise.all([
|
||||
dhApi.query.Beefy.ValidatorSetId.getValue({ at: blockHash }),
|
||||
dhApi.query.Beefy.Authorities.getValue({ at: blockHash }),
|
||||
dhApi.query.Beefy.NextAuthorities.getValue({ at: blockHash })
|
||||
]);
|
||||
|
||||
// Validate results
|
||||
invariant(validatorSetId !== undefined, "Failed to fetch BEEFY ValidatorSetId");
|
||||
logger.success(`Current ValidatorSetId: ${validatorSetId}`);
|
||||
|
||||
invariant(
|
||||
authoritiesRaw && authoritiesRaw.length > 0,
|
||||
"No BEEFY Authorities found on the chain"
|
||||
);
|
||||
const currentAuthorityKeys = authoritiesRaw.map((key) => key.asHex());
|
||||
logger.success(`Found ${currentAuthorityKeys.length} current BEEFY authorities`);
|
||||
|
||||
invariant(
|
||||
nextAuthoritiesRaw && nextAuthoritiesRaw.length > 0,
|
||||
"No BEEFY NextAuthorities found on the chain"
|
||||
);
|
||||
const nextAuthorityKeys = nextAuthoritiesRaw.map((key) => key.asHex());
|
||||
logger.success(`Found ${nextAuthorityKeys.length} next BEEFY authorities`);
|
||||
|
||||
// Calculate minimum required signatures based on validator count
|
||||
// Uses Snowbridge's formula: n - floor((n-1)/3) for strictly > 2/3 majority
|
||||
const minNumRequiredSignatures = calculateMinRequiredSignatures(currentAuthorityKeys.length);
|
||||
logger.info(
|
||||
`📊 Minimum required signatures: ${minNumRequiredSignatures} (${currentAuthorityKeys.length} - floor((${currentAuthorityKeys.length}-1)/3))`
|
||||
);
|
||||
|
||||
// Compute hashes for both sets
|
||||
logger.info("🔐 Computing authority hashes for current set...");
|
||||
const initialValidatorHashes = computeAuthorityHashes(currentAuthorityKeys);
|
||||
|
||||
logger.info("🔐 Computing authority hashes for next set...");
|
||||
const nextValidatorHashes = computeAuthorityHashes(nextAuthorityKeys);
|
||||
|
||||
// Check if the sets are identical
|
||||
const setsAreIdentical =
|
||||
JSON.stringify(initialValidatorHashes) === JSON.stringify(nextValidatorHashes);
|
||||
if (setsAreIdentical) {
|
||||
logger.info("ℹ️ Current and next authority sets are identical");
|
||||
} else {
|
||||
logger.info("ℹ️ Current and next authority sets differ");
|
||||
}
|
||||
|
||||
return {
|
||||
startBlock,
|
||||
minNumRequiredSignatures,
|
||||
initialValidatorSetId: Number(validatorSetId),
|
||||
initialValidatorHashes,
|
||||
nextValidatorSetId: Number(validatorSetId) + 1,
|
||||
nextValidatorHashes
|
||||
};
|
||||
} finally {
|
||||
papiClient.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the config file with the fetched BEEFY checkpoint data.
|
||||
*
|
||||
* @param networkId - The network identifier (e.g., "hoodi", "stagenet-hoodi")
|
||||
* @param checkpointData - BEEFY checkpoint data including validator set IDs, hashes, startBlock, and minNumRequiredSignatures
|
||||
*/
|
||||
const updateConfigFile = async (
|
||||
networkId: string,
|
||||
checkpointData: BeefyCheckpointData
|
||||
): Promise<void> => {
|
||||
const configFilePath = `../contracts/config/${networkId}.json`;
|
||||
const configFile = Bun.file(configFilePath);
|
||||
|
||||
if (!(await configFile.exists())) {
|
||||
throw new Error(`Configuration file not found: ${configFilePath}`);
|
||||
}
|
||||
|
||||
const configContent = await configFile.text();
|
||||
const configJson = JSON.parse(configContent);
|
||||
|
||||
if (!configJson.snowbridge) {
|
||||
logger.warn(`"snowbridge" section not found in config, creating it.`);
|
||||
configJson.snowbridge = {};
|
||||
}
|
||||
|
||||
// Store the old values for comparison
|
||||
const oldStartBlock = configJson.snowbridge.startBlock;
|
||||
const oldMinSigs = configJson.snowbridge.minNumRequiredSignatures;
|
||||
const oldInitialId = configJson.snowbridge.initialValidatorSetId;
|
||||
const oldNextId = configJson.snowbridge.nextValidatorSetId;
|
||||
const oldInitial = configJson.snowbridge.initialValidatorHashes || [];
|
||||
const oldNext = configJson.snowbridge.nextValidatorHashes || [];
|
||||
|
||||
// Update with new values
|
||||
configJson.snowbridge.startBlock = checkpointData.startBlock;
|
||||
configJson.snowbridge.minNumRequiredSignatures = checkpointData.minNumRequiredSignatures;
|
||||
configJson.snowbridge.initialValidatorSetId = checkpointData.initialValidatorSetId;
|
||||
configJson.snowbridge.initialValidatorHashes = checkpointData.initialValidatorHashes;
|
||||
configJson.snowbridge.nextValidatorSetId = checkpointData.nextValidatorSetId;
|
||||
configJson.snowbridge.nextValidatorHashes = checkpointData.nextValidatorHashes;
|
||||
|
||||
await Bun.write(configFilePath, `${JSON.stringify(configJson, null, 2)}\n`);
|
||||
|
||||
logger.success(`Config file updated: ${configFilePath}`);
|
||||
|
||||
// Show what changed
|
||||
if (oldStartBlock !== checkpointData.startBlock) {
|
||||
logger.info(` startBlock: ${oldStartBlock ?? "unset"} -> ${checkpointData.startBlock}`);
|
||||
}
|
||||
if (oldMinSigs !== checkpointData.minNumRequiredSignatures) {
|
||||
logger.info(
|
||||
` minNumRequiredSignatures: ${oldMinSigs ?? "unset"} -> ${checkpointData.minNumRequiredSignatures}`
|
||||
);
|
||||
}
|
||||
if (oldInitialId !== checkpointData.initialValidatorSetId) {
|
||||
logger.info(
|
||||
` initialValidatorSetId: ${oldInitialId ?? "unset"} -> ${checkpointData.initialValidatorSetId}`
|
||||
);
|
||||
}
|
||||
if (oldNextId !== checkpointData.nextValidatorSetId) {
|
||||
logger.info(
|
||||
` nextValidatorSetId: ${oldNextId ?? "unset"} -> ${checkpointData.nextValidatorSetId}`
|
||||
);
|
||||
}
|
||||
if (JSON.stringify(oldInitial) !== JSON.stringify(checkpointData.initialValidatorHashes)) {
|
||||
logger.info(
|
||||
` initialValidatorHashes: ${oldInitial.length} -> ${checkpointData.initialValidatorHashes.length} entries`
|
||||
);
|
||||
}
|
||||
if (JSON.stringify(oldNext) !== JSON.stringify(checkpointData.nextValidatorHashes)) {
|
||||
logger.info(
|
||||
` nextValidatorHashes: ${oldNext.length} -> ${checkpointData.nextValidatorHashes.length} entries`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Main handler for the update-beefy-checkpoint command.
|
||||
* Fetches BEEFY authorities from a live DataHaven chain and updates the config file.
|
||||
*/
|
||||
export const updateBeefyCheckpoint = async (
|
||||
options: UpdateBeefyCheckpointOptions
|
||||
): Promise<void> => {
|
||||
const networkId = buildNetworkId(options.chain, options.environment);
|
||||
|
||||
printHeader(`Updating BEEFY Checkpoint for ${networkId}`);
|
||||
|
||||
logger.info("📋 Configuration:");
|
||||
logger.info(` Chain: ${options.chain}`);
|
||||
if (options.environment) {
|
||||
logger.info(` Environment: ${options.environment}`);
|
||||
}
|
||||
logger.info(` RPC URL: ${options.rpcUrl}`);
|
||||
logger.info(` Config file: contracts/config/${networkId}.json`);
|
||||
|
||||
printDivider();
|
||||
|
||||
try {
|
||||
// Fetch checkpoint data from the live chain
|
||||
const checkpointData = await fetchBeefyCheckpointData(options.rpcUrl);
|
||||
|
||||
printDivider();
|
||||
|
||||
// Display the checkpoint data
|
||||
logger.info("📝 BEEFY Checkpoint Data:");
|
||||
logger.info(` Start Block: ${checkpointData.startBlock}`);
|
||||
logger.info(` Min Required Signatures: ${checkpointData.minNumRequiredSignatures}`);
|
||||
logger.info(` Initial Validator Set ID: ${checkpointData.initialValidatorSetId}`);
|
||||
logger.info(` Initial Validators (${checkpointData.initialValidatorHashes.length} total):`);
|
||||
for (let i = 0; i < checkpointData.initialValidatorHashes.length; i++) {
|
||||
logger.info(` [${i}] ${checkpointData.initialValidatorHashes[i]}`);
|
||||
}
|
||||
|
||||
logger.info(` Next Validator Set ID: ${checkpointData.nextValidatorSetId}`);
|
||||
logger.info(` Next Validators (${checkpointData.nextValidatorHashes.length} total):`);
|
||||
for (let i = 0; i < checkpointData.nextValidatorHashes.length; i++) {
|
||||
logger.info(` [${i}] ${checkpointData.nextValidatorHashes[i]}`);
|
||||
}
|
||||
|
||||
printDivider();
|
||||
|
||||
// Update the config file
|
||||
await updateConfigFile(networkId, checkpointData);
|
||||
|
||||
printDivider();
|
||||
logger.success(`BEEFY checkpoint updated successfully for ${networkId}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update BEEFY checkpoint: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* CLI action handler for the update-beefy-checkpoint command.
|
||||
* Note: Chain and environment validation is handled by contractsPreActionHook.
|
||||
*/
|
||||
export const contractsUpdateBeefyCheckpoint = async (
|
||||
options: any,
|
||||
_command: any
|
||||
): Promise<void> => {
|
||||
const { chain, environment, rpcUrl } = options;
|
||||
|
||||
// Validate rpc-url (specific to this command, not validated by preAction hook)
|
||||
if (!rpcUrl) {
|
||||
logger.error("❌ --rpc-url is required (WebSocket URL to the DataHaven chain)");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await updateBeefyCheckpoint({
|
||||
chain,
|
||||
environment,
|
||||
rpcUrl
|
||||
});
|
||||
};
|
||||
|
|
@ -3,8 +3,15 @@ import { deployContracts } from "../../../scripts/deploy-contracts";
|
|||
import { showDeploymentPlanAndStatus } from "./status";
|
||||
import { verifyContracts } from "./verify";
|
||||
|
||||
export const contractsDeploy = async (options: any, command: any) => {
|
||||
// Try to get chain from options or command
|
||||
/**
|
||||
* Extracts chain and environment options from command options and parent command.
|
||||
* This handles the case where options may be specified at either the subcommand
|
||||
* or parent command level.
|
||||
*/
|
||||
const getChainAndEnvironment = (
|
||||
options: any,
|
||||
command: any
|
||||
): { chain: string | undefined; environment: string | undefined } => {
|
||||
let chain = options.chain;
|
||||
if (!chain && command.parent) {
|
||||
chain = command.parent.getOptionValue("chain");
|
||||
|
|
@ -13,19 +20,38 @@ export const contractsDeploy = async (options: any, command: any) => {
|
|||
chain = command.getOptionValue("chain");
|
||||
}
|
||||
|
||||
printHeader(`Deploying DataHaven Contracts to ${chain}`);
|
||||
let environment = options.environment;
|
||||
if (!environment && command.parent) {
|
||||
environment = command.parent.getOptionValue("environment");
|
||||
}
|
||||
|
||||
return { chain, environment };
|
||||
};
|
||||
|
||||
export const contractsDeploy = async (options: any, command: any) => {
|
||||
const { chain, environment } = getChainAndEnvironment(options, command);
|
||||
|
||||
// Build display name for logging
|
||||
const displayName = environment ? `${environment}-${chain}` : chain;
|
||||
|
||||
printHeader(`Deploying DataHaven Contracts to ${displayName}`);
|
||||
|
||||
const txExecutionOverride = options.executeOwnerTransactions ? true : undefined;
|
||||
|
||||
try {
|
||||
logger.info("🚀 Starting deployment...");
|
||||
logger.info(`📡 Using chain: ${chain}`);
|
||||
if (environment) {
|
||||
logger.info(`📡 Using environment: ${environment}`);
|
||||
}
|
||||
if (options.rpcUrl) {
|
||||
logger.info(`📡 Using RPC URL: ${options.rpcUrl}`);
|
||||
}
|
||||
|
||||
// Chain is guaranteed to be defined by preAction hook validation
|
||||
await deployContracts({
|
||||
chain: chain,
|
||||
chain: chain!,
|
||||
environment: environment,
|
||||
rpcUrl: options.rpcUrl,
|
||||
privateKey: options.privateKey,
|
||||
avsOwnerKey: options.avsOwnerKey,
|
||||
|
|
@ -40,34 +66,27 @@ export const contractsDeploy = async (options: any, command: any) => {
|
|||
};
|
||||
|
||||
export const contractsCheck = async (options: any, command: any) => {
|
||||
// Try to get chain from options or command
|
||||
let chain = options.chain;
|
||||
if (!chain && command.parent) {
|
||||
chain = command.parent.getOptionValue("chain");
|
||||
}
|
||||
if (!chain) {
|
||||
chain = command.getOptionValue("chain");
|
||||
}
|
||||
const { chain, environment } = getChainAndEnvironment(options, command);
|
||||
|
||||
printHeader(`Checking DataHaven ${chain} Configuration and Status`);
|
||||
// Build network identifier with environment prefix if specified
|
||||
const networkId = environment ? `${environment}-${chain}` : chain;
|
||||
|
||||
printHeader(`Checking DataHaven ${networkId} Configuration and Status`);
|
||||
|
||||
logger.info("🔍 Showing deployment plan and status");
|
||||
|
||||
// Use the status function from status.ts
|
||||
await showDeploymentPlanAndStatus(chain);
|
||||
// Chain is guaranteed to be defined by preAction hook validation
|
||||
await showDeploymentPlanAndStatus(chain!, environment);
|
||||
};
|
||||
|
||||
export const contractsVerify = async (options: any, command: any) => {
|
||||
// Try to get chain from options or command
|
||||
let chain = options.chain;
|
||||
if (!chain && command.parent) {
|
||||
chain = command.parent.getOptionValue("chain");
|
||||
}
|
||||
if (!chain) {
|
||||
chain = command.getOptionValue("chain");
|
||||
}
|
||||
const { chain, environment } = getChainAndEnvironment(options, command);
|
||||
|
||||
printHeader(`Verifying DataHaven Contracts on ${chain} Block Explorer`);
|
||||
// Build display name for logging
|
||||
const displayName = environment ? `${environment}-${chain}` : chain;
|
||||
|
||||
printHeader(`Verifying DataHaven Contracts on ${displayName} Block Explorer`);
|
||||
|
||||
if (options.skipVerification) {
|
||||
logger.info("⏭️ Skipping verification as requested");
|
||||
|
|
@ -77,7 +96,8 @@ export const contractsVerify = async (options: any, command: any) => {
|
|||
try {
|
||||
const verifyOptions = {
|
||||
...options,
|
||||
chain: chain
|
||||
chain: chain,
|
||||
environment: environment
|
||||
};
|
||||
await verifyContracts(verifyOptions);
|
||||
printDivider();
|
||||
|
|
@ -86,26 +106,63 @@ export const contractsVerify = async (options: any, command: any) => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Supported networks for contract deployment.
|
||||
* These must correspond to config files in contracts/config/{network}.json
|
||||
*/
|
||||
export const SUPPORTED_NETWORKS = [
|
||||
"anvil",
|
||||
"hoodi",
|
||||
"stagenet-hoodi",
|
||||
"testnet-hoodi",
|
||||
"ethereum",
|
||||
"mainnet-ethereum"
|
||||
] as const;
|
||||
|
||||
export const contractsPreActionHook = async (thisCommand: any) => {
|
||||
let chain = thisCommand.getOptionValue("chain");
|
||||
let environment = thisCommand.getOptionValue("environment");
|
||||
|
||||
if (!chain && thisCommand.parent) {
|
||||
chain = thisCommand.parent.getOptionValue("chain");
|
||||
}
|
||||
if (!environment && thisCommand.parent) {
|
||||
environment = thisCommand.parent.getOptionValue("environment");
|
||||
}
|
||||
|
||||
const privateKey = thisCommand.getOptionValue("privateKey");
|
||||
|
||||
if (!chain) {
|
||||
logger.error("❌ Chain is required. Use --chain option (hoodi, mainnet, anvil)");
|
||||
logger.error("❌ Chain is required. Use --chain option (hoodi, ethereum, anvil)");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const supportedChains = ["hoodi", "mainnet", "anvil"];
|
||||
const supportedChains = ["hoodi", "ethereum", "anvil"];
|
||||
if (!supportedChains.includes(chain)) {
|
||||
logger.error(`❌ Unsupported chain: ${chain}. Supported chains: ${supportedChains.join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate environment if provided
|
||||
if (environment) {
|
||||
const supportedEnvironments = ["stagenet", "testnet", "mainnet"];
|
||||
if (!supportedEnvironments.includes(environment)) {
|
||||
logger.error(
|
||||
`❌ Unsupported environment: ${environment}. Supported environments: ${supportedEnvironments.join(", ")}`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate the full network identifier exists
|
||||
const networkId = `${environment}-${chain}`;
|
||||
if (!SUPPORTED_NETWORKS.includes(networkId as (typeof SUPPORTED_NETWORKS)[number])) {
|
||||
logger.error(
|
||||
`❌ Unsupported network combination: ${networkId}. Supported networks: ${SUPPORTED_NETWORKS.join(", ")}`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!privateKey && !process.env.DEPLOYER_PRIVATE_KEY) {
|
||||
logger.warn(
|
||||
"⚠️ Private key not provided. Will use DEPLOYER_PRIVATE_KEY environment variable if set, or default Anvil key."
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
export * from "./beefy-checkpoint";
|
||||
export * from "./deploy";
|
||||
export * from "./rewards-origin";
|
||||
export * from "./status";
|
||||
export * from "./update-metadata";
|
||||
export * from "./verify";
|
||||
|
|
|
|||
327
test/cli/handlers/contracts/rewards-origin.ts
Normal file
327
test/cli/handlers/contracts/rewards-origin.ts
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
import invariant from "tiny-invariant";
|
||||
import { logger, printDivider, printHeader } from "utils";
|
||||
import { createPapiConnectors } from "utils/papi";
|
||||
import { concat, type Hex, toBytes, toHex } from "viem";
|
||||
import { buildNetworkId } from "../../../configs/contracts/config";
|
||||
|
||||
interface UpdateRewardsOriginOptions {
|
||||
chain: string;
|
||||
environment?: string;
|
||||
rpcUrl: string;
|
||||
genesisHash?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives an AccountId20 from a PalletId using the same algorithm as Substrate's
|
||||
* `into_account_truncating()`.
|
||||
*
|
||||
* The algorithm (see https://www.shawntabrizi.com/substrate-js-utilities/):
|
||||
* 1. Prepends "modl" (4 bytes) to the 8-byte pallet ID
|
||||
* 2. For AccountId20 (H160), takes the first 20 bytes: modl(4) + pallet_id(8) + zeros(8)
|
||||
*
|
||||
* Note: This is a simple truncation, NOT a hash operation.
|
||||
*
|
||||
* @param palletId - The 8-character pallet ID string (e.g., "dh/evrew")
|
||||
* @returns The derived AccountId20 as a hex string
|
||||
*/
|
||||
const palletIdToAccountId20 = (palletId: string): Hex => {
|
||||
invariant(palletId.length === 8, "Pallet ID must be exactly 8 characters");
|
||||
|
||||
// Build: "modl" (4 bytes) + pallet_id (8 bytes) + zeros (8 bytes) = 20 bytes
|
||||
const prefix = toBytes("modl");
|
||||
const palletIdBytes = toBytes(palletId);
|
||||
const accountId20 = new Uint8Array(20);
|
||||
accountId20.set(prefix, 0);
|
||||
accountId20.set(palletIdBytes, 4);
|
||||
// Remaining 8 bytes are already zeros (padding)
|
||||
|
||||
return toHex(accountId20);
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the Agent ID (H256) for a pallet's sovereign account on the DataHaven chain.
|
||||
*
|
||||
* The Agent ID is computed following Snowbridge's `AgentIdOf` type, which uses
|
||||
* `HashedDescription` with `DescribeGlobalPrefix`. For an AccountKey20 on a chain
|
||||
* identified by its genesis hash, the encoding is:
|
||||
*
|
||||
* blake2_256(SCALE_ENCODE(("GlobalConsensus", ByGenesis(genesis_hash), ("AccountKey20", account_key))))
|
||||
*
|
||||
* NOTE: This computation follows Snowbridge's pattern but may need verification against
|
||||
* the actual on-chain Agent ID. The preferred approach is to set RewardsAgentOrigin on
|
||||
* the chain and fetch it via this command.
|
||||
*
|
||||
* @param genesisHash - The chain's genesis hash (32 bytes, hex string with 0x prefix)
|
||||
* @param accountKey20 - The 20-byte account key (hex string with 0x prefix)
|
||||
* @returns The computed Agent ID as a hex string
|
||||
*/
|
||||
const computeAgentId = async (genesisHash: Hex, accountKey20: Hex): Promise<Hex> => {
|
||||
// Import blake2b dynamically (it's an ESM module)
|
||||
const { blake2b } = await import("@noble/hashes/blake2b");
|
||||
|
||||
// Validate inputs
|
||||
invariant(
|
||||
genesisHash.length === 66,
|
||||
`Genesis hash must be 32 bytes (66 chars with 0x prefix), got ${genesisHash.length}`
|
||||
);
|
||||
invariant(
|
||||
accountKey20.length === 42,
|
||||
`Account key must be 20 bytes (42 chars with 0x prefix), got ${accountKey20.length}`
|
||||
);
|
||||
|
||||
// SCALE encoding for the location description follows Snowbridge's pattern:
|
||||
// ("GlobalConsensus", ByGenesis(genesis_hash), interior_description)
|
||||
//
|
||||
// Where interior_description for AccountKey20 is:
|
||||
// ("AccountKey20", key)
|
||||
//
|
||||
// In SCALE for fixed-size arrays (like b"GlobalConsensus"):
|
||||
// - Fixed-size byte arrays are encoded as raw bytes without length prefix
|
||||
// - Variable-length Vec<u8> gets a compact length prefix
|
||||
// - Enums are encoded as variant index + payload
|
||||
|
||||
// "GlobalConsensus" as raw bytes (15 bytes, no length prefix for fixed array)
|
||||
const globalConsensusBytes = toBytes("GlobalConsensus");
|
||||
|
||||
// ByGenesis variant (index 0 in NetworkId enum) + genesis hash (32 bytes)
|
||||
// NetworkId::ByGenesis is the first variant, so index = 0
|
||||
const byGenesisVariant = new Uint8Array([0]);
|
||||
const genesisBytes = toBytes(genesisHash);
|
||||
|
||||
// "AccountKey20" as raw bytes (12 bytes, no length prefix for fixed array)
|
||||
const accountKey20StrBytes = toBytes("AccountKey20");
|
||||
|
||||
// Account key bytes (20 bytes)
|
||||
const accountKeyBytes = toBytes(accountKey20);
|
||||
|
||||
// Build the interior description: ("AccountKey20", key) as raw bytes
|
||||
const interiorDescription = concat([accountKey20StrBytes, accountKeyBytes]);
|
||||
|
||||
// Length prefix for interior (SCALE compact encoding: value << 2 for values < 64)
|
||||
const interiorLen = interiorDescription.length;
|
||||
const interiorLenCompact = new Uint8Array([interiorLen << 2]);
|
||||
|
||||
// Final encoding: GlobalConsensus prefix + ByGenesis(genesis) + compact_len(interior)
|
||||
const encoded = concat([
|
||||
globalConsensusBytes,
|
||||
byGenesisVariant,
|
||||
genesisBytes,
|
||||
interiorLenCompact,
|
||||
interiorDescription
|
||||
]);
|
||||
|
||||
// Hash with blake2b-256 to get the Agent ID (same as Snowbridge's blake2_256)
|
||||
const hash = blake2b(new Uint8Array(encoded), { dkLen: 32 });
|
||||
return toHex(hash);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the RewardsAgentOrigin from the runtime parameters.
|
||||
*
|
||||
* @param rpcUrl - WebSocket RPC endpoint of the DataHaven chain
|
||||
* @returns The RewardsAgentOrigin as a hex string, or null if not set or zero
|
||||
*/
|
||||
const fetchRewardsAgentOrigin = async (rpcUrl: string): Promise<Hex | null> => {
|
||||
logger.info(`📡 Connecting to DataHaven chain at ${rpcUrl}...`);
|
||||
|
||||
const { client: papiClient, typedApi: dhApi } = createPapiConnectors(rpcUrl);
|
||||
|
||||
try {
|
||||
logger.info("🔍 Fetching RewardsAgentOrigin from runtime parameters...");
|
||||
|
||||
// Query the Parameters pallet for RewardsAgentOrigin
|
||||
const parameter = await dhApi.query.Parameters.Parameters.getValue(
|
||||
{
|
||||
type: "RuntimeConfig",
|
||||
value: { type: "RewardsAgentOrigin", value: undefined }
|
||||
},
|
||||
{ at: "best" }
|
||||
);
|
||||
|
||||
if (!parameter) {
|
||||
logger.info("ℹ️ RewardsAgentOrigin parameter not found (using default)");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract the value from the parameter result
|
||||
// The parameter is wrapped in the RuntimeConfig enum variant
|
||||
if (parameter.type === "RuntimeConfig" && parameter.value.type === "RewardsAgentOrigin") {
|
||||
const origin = parameter.value.value;
|
||||
if (origin) {
|
||||
const originHex = origin.asHex();
|
||||
// Check if it's the zero hash
|
||||
const zeroHash =
|
||||
"0x0000000000000000000000000000000000000000000000000000000000000000" as Hex;
|
||||
if (originHex === zeroHash) {
|
||||
logger.info("ℹ️ RewardsAgentOrigin is set to zero (placeholder)");
|
||||
return null;
|
||||
}
|
||||
logger.success(`Found RewardsAgentOrigin: ${originHex}`);
|
||||
return originHex as Hex;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("ℹ️ RewardsAgentOrigin value not available");
|
||||
return null;
|
||||
} finally {
|
||||
papiClient.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the genesis hash from the chain.
|
||||
*
|
||||
* @param rpcUrl - WebSocket RPC endpoint of the DataHaven chain
|
||||
* @returns The genesis hash as a hex string
|
||||
*/
|
||||
const fetchGenesisHash = async (rpcUrl: string): Promise<Hex> => {
|
||||
logger.info("🔍 Fetching genesis hash from chain...");
|
||||
|
||||
const { client: papiClient } = createPapiConnectors(rpcUrl);
|
||||
|
||||
try {
|
||||
// Use _request to call chain_getBlockHash RPC method with block number 0
|
||||
const genesisHash = await papiClient._request<string>("chain_getBlockHash", [0]);
|
||||
logger.success(`Genesis hash: ${genesisHash}`);
|
||||
return genesisHash as Hex;
|
||||
} finally {
|
||||
papiClient.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the config file with the rewards message origin.
|
||||
*
|
||||
* @param networkId - The network identifier (e.g., "hoodi", "stagenet-hoodi")
|
||||
* @param rewardsMessageOrigin - The rewards message origin (Agent ID)
|
||||
*/
|
||||
const updateConfigFile = async (networkId: string, rewardsMessageOrigin: Hex): Promise<void> => {
|
||||
const configFilePath = `../contracts/config/${networkId}.json`;
|
||||
const configFile = Bun.file(configFilePath);
|
||||
|
||||
if (!(await configFile.exists())) {
|
||||
throw new Error(`Configuration file not found: ${configFilePath}`);
|
||||
}
|
||||
|
||||
const configContent = await configFile.text();
|
||||
const configJson = JSON.parse(configContent);
|
||||
|
||||
if (!configJson.snowbridge) {
|
||||
logger.warn(`"snowbridge" section not found in config, creating it.`);
|
||||
configJson.snowbridge = {};
|
||||
}
|
||||
|
||||
const oldOrigin = configJson.snowbridge.rewardsMessageOrigin;
|
||||
configJson.snowbridge.rewardsMessageOrigin = rewardsMessageOrigin;
|
||||
|
||||
await Bun.write(configFilePath, `${JSON.stringify(configJson, null, 2)}\n`);
|
||||
|
||||
logger.success(`Config file updated: ${configFilePath}`);
|
||||
|
||||
if (oldOrigin !== rewardsMessageOrigin) {
|
||||
logger.info(` rewardsMessageOrigin: ${oldOrigin ?? "unset"} -> ${rewardsMessageOrigin}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Main handler for the update-rewards-origin command.
|
||||
* Fetches or computes the RewardsAgentOrigin and updates the config file.
|
||||
*/
|
||||
export const updateRewardsOrigin = async (options: UpdateRewardsOriginOptions): Promise<void> => {
|
||||
const networkId = buildNetworkId(options.chain, options.environment);
|
||||
|
||||
printHeader(`Updating Rewards Message Origin for ${networkId}`);
|
||||
|
||||
logger.info("📋 Configuration:");
|
||||
logger.info(` Chain: ${options.chain}`);
|
||||
if (options.environment) {
|
||||
logger.info(` Environment: ${options.environment}`);
|
||||
}
|
||||
logger.info(` RPC URL: ${options.rpcUrl}`);
|
||||
if (options.genesisHash) {
|
||||
logger.info(` Genesis hash (provided): ${options.genesisHash}`);
|
||||
}
|
||||
logger.info(` Config file: contracts/config/${networkId}.json`);
|
||||
|
||||
printDivider();
|
||||
|
||||
try {
|
||||
// Step 1: Try to fetch RewardsAgentOrigin from the chain
|
||||
let rewardsMessageOrigin = await fetchRewardsAgentOrigin(options.rpcUrl);
|
||||
|
||||
printDivider();
|
||||
|
||||
if (rewardsMessageOrigin) {
|
||||
// Use the value from the chain
|
||||
logger.info("✅ Using RewardsAgentOrigin from chain runtime parameters");
|
||||
} else {
|
||||
// Compute the Agent ID from genesis hash and pallet account
|
||||
logger.info("🔧 Computing RewardsAgentOrigin from genesis hash and pallet account...");
|
||||
|
||||
// Get genesis hash (from option or fetch from chain)
|
||||
const genesisHash = options.genesisHash
|
||||
? (options.genesisHash as Hex)
|
||||
: await fetchGenesisHash(options.rpcUrl);
|
||||
|
||||
// Derive the ExternalValidatorRewardsAccount from the pallet ID "dh/evrew"
|
||||
const palletId = "dh/evrew";
|
||||
logger.info(`🔐 Deriving account from pallet ID: "${palletId}"`);
|
||||
const rewardsAccount = palletIdToAccountId20(palletId);
|
||||
logger.info(` Rewards pallet account: ${rewardsAccount}`);
|
||||
|
||||
// Compute the Agent ID
|
||||
logger.info("🔐 Computing Agent ID...");
|
||||
logger.warn(
|
||||
"⚠️ Note: Computed Agent ID may need verification. Prefer setting RewardsAgentOrigin on-chain."
|
||||
);
|
||||
rewardsMessageOrigin = await computeAgentId(genesisHash, rewardsAccount);
|
||||
logger.info(` Agent ID: ${rewardsMessageOrigin}`);
|
||||
}
|
||||
|
||||
printDivider();
|
||||
|
||||
// Display the final value
|
||||
logger.info("📝 Rewards Message Origin:");
|
||||
logger.info(` ${rewardsMessageOrigin}`);
|
||||
|
||||
printDivider();
|
||||
|
||||
// Update the config file
|
||||
await updateConfigFile(networkId, rewardsMessageOrigin);
|
||||
|
||||
printDivider();
|
||||
logger.success(`Rewards message origin updated successfully for ${networkId}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update rewards message origin: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* CLI action handler for the update-rewards-origin command.
|
||||
* Note: Chain and environment validation is handled by contractsPreActionHook.
|
||||
*/
|
||||
export const contractsUpdateRewardsOrigin = async (options: any, _command: any): Promise<void> => {
|
||||
const { chain, environment, rpcUrl, genesisHash } = options;
|
||||
|
||||
// Validate rpc-url (specific to this command, not validated by preAction hook)
|
||||
if (!rpcUrl) {
|
||||
logger.error("❌ --rpc-url is required (WebSocket URL to the DataHaven chain)");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate genesis hash format if provided
|
||||
if (genesisHash) {
|
||||
if (!/^0x[0-9a-fA-F]{64}$/.test(genesisHash)) {
|
||||
logger.error("❌ --genesis-hash must be a 32-byte hex string (0x + 64 hex chars)");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
await updateRewardsOrigin({
|
||||
chain,
|
||||
environment,
|
||||
rpcUrl,
|
||||
genesisHash
|
||||
});
|
||||
};
|
||||
|
|
@ -1,16 +1,24 @@
|
|||
import { logger, printDivider } from "utils";
|
||||
import { getChainDeploymentParams, loadChainConfig } from "../../../configs/contracts/config";
|
||||
import {
|
||||
buildNetworkId,
|
||||
getChainDeploymentParams,
|
||||
loadChainConfig
|
||||
} from "../../../configs/contracts/config";
|
||||
import { checkContractVerification } from "./verify";
|
||||
|
||||
/**
|
||||
* Shows the status of chain deployment and verification
|
||||
* @param chain - The target chain (hoodi, mainnet, anvil)
|
||||
* @param environment - Optional deployment environment (stagenet, testnet, mainnet)
|
||||
*/
|
||||
export const showDeploymentPlanAndStatus = async (chain: string) => {
|
||||
export const showDeploymentPlanAndStatus = async (chain: string, environment?: string) => {
|
||||
const networkId = buildNetworkId(chain, environment);
|
||||
|
||||
try {
|
||||
const config = await loadChainConfig(chain);
|
||||
const config = await loadChainConfig(chain, environment);
|
||||
const deploymentParams = getChainDeploymentParams(chain);
|
||||
|
||||
const displayData = {
|
||||
const displayData: Record<string, string> = {
|
||||
Network: `${deploymentParams.network} (Chain ID: ${deploymentParams.chainId})`,
|
||||
"RPC URL": deploymentParams.rpcUrl,
|
||||
"Block Explorer": deploymentParams.blockExplorer,
|
||||
|
|
@ -19,19 +27,24 @@ export const showDeploymentPlanAndStatus = async (chain: string) => {
|
|||
"Rewards Initiator": `${config.avs.rewardsInitiator.slice(0, 10)}...${config.avs.rewardsInitiator.slice(-8)}`,
|
||||
"Veto Committee Member": `${config.avs.vetoCommitteeMember.slice(0, 10)}...${config.avs.vetoCommitteeMember.slice(-8)}`
|
||||
};
|
||||
|
||||
if (environment) {
|
||||
displayData.Environment = environment;
|
||||
}
|
||||
|
||||
console.table(displayData);
|
||||
|
||||
await showDatahavenContractStatus(chain, deploymentParams.rpcUrl);
|
||||
await showDatahavenContractStatus(networkId, deploymentParams.rpcUrl);
|
||||
await showEigenLayerContractStatus(
|
||||
config,
|
||||
deploymentParams.chainId.toString(),
|
||||
deploymentParams.rpcUrl,
|
||||
chain
|
||||
networkId
|
||||
);
|
||||
|
||||
printDivider();
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to load ${chain} configuration: ${error}`);
|
||||
logger.error(`❌ Failed to load ${networkId} configuration: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -69,8 +82,10 @@ const printContractStatus = async (
|
|||
|
||||
/**
|
||||
* Shows the status of all contracts (deployment + verification)
|
||||
* @param networkId - The network identifier (e.g., "hoodi", "stagenet-hoodi")
|
||||
* @param rpcUrl - The RPC URL for the chain
|
||||
*/
|
||||
const showDatahavenContractStatus = async (chain: string, rpcUrl: string) => {
|
||||
const showDatahavenContractStatus = async (networkId: string, rpcUrl: string) => {
|
||||
try {
|
||||
const contracts = [
|
||||
{ name: "DataHavenServiceManager", key: "ServiceManagerImplementation" },
|
||||
|
|
@ -81,7 +96,7 @@ const showDatahavenContractStatus = async (chain: string, rpcUrl: string) => {
|
|||
|
||||
logger.info("DataHaven contracts");
|
||||
|
||||
const deploymentsPath = `../contracts/deployments/${chain}.json`;
|
||||
const deploymentsPath = `../contracts/deployments/${networkId}.json`;
|
||||
const deploymentsFile = Bun.file(deploymentsPath);
|
||||
const exists = await deploymentsFile.exists();
|
||||
|
||||
|
|
@ -97,7 +112,12 @@ const showDatahavenContractStatus = async (chain: string, rpcUrl: string) => {
|
|||
|
||||
for (const contract of contracts) {
|
||||
const address = deployments[contract.key];
|
||||
await printContractStatus({ name: contract.name, address }, etherscanApiKey, chain, rpcUrl);
|
||||
await printContractStatus(
|
||||
{ name: contract.name, address },
|
||||
etherscanApiKey,
|
||||
networkId,
|
||||
rpcUrl
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`⚠️ Could not check contract status: ${error}`);
|
||||
|
|
@ -106,22 +126,26 @@ const showDatahavenContractStatus = async (chain: string, rpcUrl: string) => {
|
|||
|
||||
/**
|
||||
* Shows the status of EigenLayer contracts (verification only)
|
||||
* @param config - The chain configuration
|
||||
* @param chainId - The chain ID
|
||||
* @param rpcUrl - The RPC URL for the chain
|
||||
* @param networkId - The network identifier (e.g., "hoodi", "stagenet-hoodi")
|
||||
*/
|
||||
const showEigenLayerContractStatus = async (
|
||||
config: any,
|
||||
chainId: string,
|
||||
rpcUrl: string,
|
||||
chain: string
|
||||
networkId: string
|
||||
) => {
|
||||
try {
|
||||
// For local/anvil deployments, read addresses from deployments file
|
||||
// For testnet/mainnet, use addresses from config file
|
||||
let eigenLayerAddresses: Record<string, string> = {};
|
||||
const isLocal = chain === "anvil" || chain === "local";
|
||||
const isLocal = networkId === "anvil" || networkId === "local";
|
||||
|
||||
if (isLocal) {
|
||||
try {
|
||||
const deploymentsPath = `../contracts/deployments/${chain === "local" ? "anvil" : chain}.json`;
|
||||
const deploymentsPath = `../contracts/deployments/${networkId === "local" ? "anvil" : networkId}.json`;
|
||||
const deploymentsFile = Bun.file(deploymentsPath);
|
||||
if (await deploymentsFile.exists()) {
|
||||
const deployments = await deploymentsFile.json();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { execSync } from "node:child_process";
|
||||
import { logger } from "utils";
|
||||
import { parseDeploymentsFile } from "utils/contracts";
|
||||
import { CHAIN_CONFIGS, getChainConfig } from "../../../configs/contracts/config";
|
||||
import { buildNetworkId, CHAIN_CONFIGS, getChainConfig } from "../../../configs/contracts/config";
|
||||
|
||||
interface ContractsVerifyOptions {
|
||||
chain: string;
|
||||
environment?: string;
|
||||
rpcUrl?: string;
|
||||
skipVerification: boolean;
|
||||
}
|
||||
|
|
@ -26,7 +27,10 @@ export const verifyContracts = async (options: ContractsVerifyOptions) => {
|
|||
return;
|
||||
}
|
||||
|
||||
logger.info(`🔍 Verifying contracts on ${options.chain} block explorer using Foundry...`);
|
||||
// Build network identifier for deployment file lookup
|
||||
const networkId = buildNetworkId(options.chain, options.environment);
|
||||
|
||||
logger.info(`🔍 Verifying contracts on ${networkId} block explorer using Foundry...`);
|
||||
|
||||
const etherscanApiKey = process.env.ETHERSCAN_API_KEY;
|
||||
if (!etherscanApiKey) {
|
||||
|
|
@ -35,7 +39,7 @@ export const verifyContracts = async (options: ContractsVerifyOptions) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const deployments = await parseDeploymentsFile(options.chain);
|
||||
const deployments = await parseDeploymentsFile(networkId);
|
||||
|
||||
const contractsToVerify: ContractToVerify[] = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import {
|
|||
contractsCheck,
|
||||
contractsDeploy,
|
||||
contractsPreActionHook,
|
||||
contractsUpdateBeefyCheckpoint,
|
||||
contractsUpdateRewardsOrigin,
|
||||
contractsVerify,
|
||||
deploy,
|
||||
deployPreActionHook,
|
||||
|
|
@ -199,15 +201,20 @@ const contractsCommand = program
|
|||
.addHelpText(
|
||||
"before",
|
||||
`🫎 DataHaven: Contracts Deployment CLI for deploying DataHaven AVS contracts to supported chains
|
||||
|
||||
|
||||
Commands:
|
||||
- status: Show deployment plan, configuration, and status (default)
|
||||
- deploy: Deploy contracts to specified chain
|
||||
- verify: Verify deployed contracts on block explorer
|
||||
- update-beefy-checkpoint: Fetch BEEFY authorities from a live chain and update config
|
||||
- update-rewards-origin: Fetch or compute the RewardsAgentOrigin and update config
|
||||
- update-metadata: Update the metadata URI of an existing AVS contract
|
||||
|
||||
|
||||
Common options:
|
||||
--chain: Target chain (required: hoodi, mainnet, anvil)
|
||||
--chain: Target chain (required: hoodi, ethereum, anvil)
|
||||
--environment: Deployment environment (stagenet, testnet, mainnet)
|
||||
When specified, config files are read from {environment}-{chain}.json
|
||||
and deployments are written to {environment}-{chain}.json
|
||||
--rpc-url: Chain RPC URL (optional, defaults based on chain)
|
||||
--private-key: Private key for deployment
|
||||
--skip-verification: Skip contract verification
|
||||
|
|
@ -219,7 +226,11 @@ const contractsCommand = program
|
|||
contractsCommand
|
||||
.command("status")
|
||||
.description("Show deployment plan, configuration, and status")
|
||||
.option("--chain <value>", "Target chain (hoodi, mainnet, anvil)")
|
||||
.option("--chain <value>", "Target chain (hoodi, ethereum, anvil)")
|
||||
.option(
|
||||
"--environment <value>",
|
||||
"Deployment environment (stagenet, testnet, mainnet). Config and deployment files will be prefixed with this value."
|
||||
)
|
||||
.option("--rpc-url <value>", "Chain RPC URL (optional, defaults based on chain)")
|
||||
.option(
|
||||
"--private-key <value>",
|
||||
|
|
@ -234,7 +245,11 @@ contractsCommand
|
|||
contractsCommand
|
||||
.command("deploy")
|
||||
.description("Deploy DataHaven AVS contracts to specified chain")
|
||||
.option("--chain <value>", "Target chain (hoodi, mainnet, anvil)")
|
||||
.option("--chain <value>", "Target chain (hoodi, ethereum, anvil)")
|
||||
.option(
|
||||
"--environment <value>",
|
||||
"Deployment environment (stagenet, testnet, mainnet). Config and deployment files will be prefixed with this value."
|
||||
)
|
||||
.option("--rpc-url <value>", "Chain RPC URL (optional, defaults based on chain)")
|
||||
.option(
|
||||
"--private-key <value>",
|
||||
|
|
@ -255,17 +270,73 @@ contractsCommand
|
|||
contractsCommand
|
||||
.command("verify")
|
||||
.description("Verify deployed contracts on block explorer")
|
||||
.option("--chain <value>", "Target chain (hoodi, mainnet, anvil)")
|
||||
.option("--chain <value>", "Target chain (hoodi, ethereum, anvil)")
|
||||
.option(
|
||||
"--environment <value>",
|
||||
"Deployment environment (stagenet, testnet, mainnet). Config and deployment files will be prefixed with this value."
|
||||
)
|
||||
.option("--rpc-url <value>", "Chain RPC URL (optional, defaults based on chain)")
|
||||
.option("--skip-verification", "Skip contract verification", false)
|
||||
.hook("preAction", contractsPreActionHook)
|
||||
.action(contractsVerify);
|
||||
|
||||
// Contracts Update BEEFY Checkpoint
|
||||
contractsCommand
|
||||
.command("update-beefy-checkpoint")
|
||||
.description(
|
||||
"Fetch BEEFY authorities from a live DataHaven chain and update the config file with validator hashes"
|
||||
)
|
||||
.option("--chain <value>", "Target chain (hoodi, ethereum, anvil)")
|
||||
.option(
|
||||
"--environment <value>",
|
||||
"Deployment environment (stagenet, testnet, mainnet). Config and deployment files will be prefixed with this value."
|
||||
)
|
||||
.option(
|
||||
"--rpc-url <value>",
|
||||
"WebSocket RPC URL of the DataHaven chain to fetch BEEFY authorities from"
|
||||
)
|
||||
.hook("preAction", contractsPreActionHook)
|
||||
.action(async (_options: any, command: any) => {
|
||||
// Options are captured by parent command due to shared option names
|
||||
// Use optsWithGlobals() to get all options including inherited ones
|
||||
const opts = command.optsWithGlobals();
|
||||
await contractsUpdateBeefyCheckpoint(opts, command);
|
||||
});
|
||||
|
||||
// Contracts Update Rewards Origin
|
||||
contractsCommand
|
||||
.command("update-rewards-origin")
|
||||
.description(
|
||||
"Fetch or compute the RewardsAgentOrigin and update the config file with the rewards message origin"
|
||||
)
|
||||
.option("--chain <value>", "Target chain (hoodi, ethereum, anvil)")
|
||||
.option(
|
||||
"--environment <value>",
|
||||
"Deployment environment (stagenet, testnet, mainnet). Config and deployment files will be prefixed with this value."
|
||||
)
|
||||
.option(
|
||||
"--rpc-url <value>",
|
||||
"WebSocket RPC URL of the DataHaven chain to fetch RewardsAgentOrigin from"
|
||||
)
|
||||
.option(
|
||||
"--genesis-hash <value>",
|
||||
"Chain genesis hash (32 bytes hex). If not provided, will be fetched from the chain."
|
||||
)
|
||||
.hook("preAction", contractsPreActionHook)
|
||||
.action(async (_options: any, command: any) => {
|
||||
const opts = command.optsWithGlobals();
|
||||
await contractsUpdateRewardsOrigin(opts, command);
|
||||
});
|
||||
|
||||
// Contracts Update Metadata
|
||||
contractsCommand
|
||||
.command("update-metadata")
|
||||
.description("Update AVS metadata URI for the DataHaven Service Manager")
|
||||
.option("--chain <value>", "Target chain (hoodi, mainnet, anvil)")
|
||||
.option("--chain <value>", "Target chain (hoodi, ethereum, anvil)")
|
||||
.option(
|
||||
"--environment <value>",
|
||||
"Deployment environment (stagenet, testnet, mainnet). Config and deployment files will be prefixed with this value."
|
||||
)
|
||||
.option("--uri <value>", "New metadata URI (required)")
|
||||
.option("--reset", "Use if you want to reset the metadata URI")
|
||||
.option("--rpc-url <value>", "Chain RPC URL (optional, defaults based on chain)")
|
||||
|
|
@ -289,16 +360,24 @@ contractsCommand
|
|||
if (!chain) {
|
||||
throw new Error("--chain parameter is required");
|
||||
}
|
||||
await updateAVSMetadataURI(chain, options.uri, {
|
||||
// Build network identifier with environment prefix if specified
|
||||
const environment = options.environment;
|
||||
const networkId = environment ? `${environment}-${chain}` : chain;
|
||||
await updateAVSMetadataURI(networkId, options.uri, {
|
||||
execute: options.execute,
|
||||
avsOwnerKey: options.avsOwnerKey
|
||||
});
|
||||
});
|
||||
|
||||
// Default Contracts command (runs check)
|
||||
// Default Contracts command (runs check when no subcommand is specified)
|
||||
// preAction hook on subcommands handles validation before the action runs
|
||||
contractsCommand
|
||||
.description("Show deployment plan, configuration, and status")
|
||||
.option("--chain <value>", "Target chain (hoodi, mainnet, anvil)")
|
||||
.option("--chain <value>", "Target chain (hoodi, ethereum, anvil)")
|
||||
.option(
|
||||
"--environment <value>",
|
||||
"Deployment environment (stagenet, testnet, mainnet). Config and deployment files will be prefixed with this value."
|
||||
)
|
||||
.option("--rpc-url <value>", "Chain RPC URL (optional, defaults based on chain)")
|
||||
.option(
|
||||
"--private-key <value>",
|
||||
|
|
@ -307,7 +386,9 @@ contractsCommand
|
|||
)
|
||||
.option("--skip-verification", "Skip contract verification", false)
|
||||
.hook("preAction", contractsPreActionHook)
|
||||
.action(contractsCheck);
|
||||
.action(async (options: any, command: any) => {
|
||||
await contractsCheck(options, command);
|
||||
});
|
||||
|
||||
// ===== Exec ======
|
||||
// Disabled until need arises
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ export const CHAIN_CONFIGS = {
|
|||
EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 256,
|
||||
SYNC_COMMITTEE_SIZE: 512
|
||||
},
|
||||
mainnet: {
|
||||
NETWORK_NAME: "mainnet",
|
||||
ethereum: {
|
||||
NETWORK_NAME: "ethereum",
|
||||
CHAIN_ID: 1,
|
||||
RPC_URL: "https://eth.llamarpc.com",
|
||||
BLOCK_EXPLORER: "https://etherscan.io/",
|
||||
|
|
@ -39,22 +39,39 @@ export const getChainConfig = (chain: string) => {
|
|||
return CHAIN_CONFIGS[chain as keyof ChainConfigType];
|
||||
};
|
||||
|
||||
export const loadChainConfig = async (chain: string) => {
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads chain configuration from the config file
|
||||
* @param chain - The target chain (hoodi, mainnet, anvil)
|
||||
* @param environment - Optional deployment environment (stagenet, testnet, mainnet)
|
||||
* When specified, loads from {environment}-{chain}.json
|
||||
*/
|
||||
export const loadChainConfig = async (chain: string, environment?: string) => {
|
||||
const networkId = buildNetworkId(chain, environment);
|
||||
|
||||
try {
|
||||
const configPath = `../contracts/config/${chain}.json`;
|
||||
const configPath = `../contracts/config/${networkId}.json`;
|
||||
const configFile = Bun.file(configPath);
|
||||
|
||||
if (!(await configFile.exists())) {
|
||||
throw new Error(`${chain} configuration file not found at ${configPath}`);
|
||||
throw new Error(`${networkId} configuration file not found at ${configPath}`);
|
||||
}
|
||||
|
||||
const configContent = await configFile.text();
|
||||
const config = JSON.parse(configContent);
|
||||
|
||||
logger.debug(`✅ ${chain} configuration loaded successfully`);
|
||||
logger.debug(`✅ ${networkId} configuration loaded successfully`);
|
||||
return config;
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to load ${chain} configuration: ${error}`);
|
||||
logger.error(`❌ Failed to load ${networkId} configuration: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { dataHavenServiceManagerAbi } from "../contract-bindings/generated";
|
|||
|
||||
interface ContractDeploymentOptions {
|
||||
chain?: string;
|
||||
environment?: string;
|
||||
rpcUrl?: string;
|
||||
privateKey?: string | undefined;
|
||||
verified?: boolean;
|
||||
|
|
@ -18,6 +19,15 @@ interface ContractDeploymentOptions {
|
|||
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
|
||||
*/
|
||||
|
|
@ -52,20 +62,23 @@ export const buildContracts = async () => {
|
|||
* Constructs the deployment command
|
||||
*/
|
||||
export const constructDeployCommand = (options: ContractDeploymentOptions): string => {
|
||||
const { chain, rpcUrl, verified, blockscoutBackendUrl } = options;
|
||||
const { chain, environment, rpcUrl, verified, blockscoutBackendUrl } = options;
|
||||
|
||||
const deploymentScript =
|
||||
!chain || chain === "anvil"
|
||||
? "script/deploy/DeployLocal.s.sol"
|
||||
: "script/deploy/DeployTestnet.s.sol";
|
||||
: "script/deploy/DeployLive.s.sol";
|
||||
|
||||
logger.info(`🚀 Deploying contracts to ${chain} using ${deploymentScript}`);
|
||||
// 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 variable for chain if specified
|
||||
// Add environment variables for network (used by Solidity scripts for config/output file naming)
|
||||
if (chain) {
|
||||
deployCommand = `NETWORK=${chain} ${deployCommand}`;
|
||||
deployCommand = `NETWORK=${networkId} ${deployCommand}`;
|
||||
}
|
||||
|
||||
if (verified && blockscoutBackendUrl) {
|
||||
|
|
@ -146,6 +159,7 @@ export const updateParameters = async (
|
|||
*/
|
||||
export const deployContracts = async (options: {
|
||||
chain: string;
|
||||
environment?: string;
|
||||
rpcUrl?: string;
|
||||
privateKey?: string | undefined;
|
||||
verified?: boolean;
|
||||
|
|
@ -160,6 +174,9 @@ export const deployContracts = async (options: {
|
|||
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;
|
||||
|
|
@ -173,7 +190,7 @@ export const deployContracts = async (options: {
|
|||
}
|
||||
|
||||
if (!resolvedAvsOwnerAddress && isLocalChain) {
|
||||
const config = await loadChainConfig(options.chain);
|
||||
const config = await loadChainConfig(options.chain, options.environment);
|
||||
resolvedAvsOwnerAddress = config?.avs?.avsOwner;
|
||||
}
|
||||
|
||||
|
|
@ -191,6 +208,7 @@ export const deployContracts = async (options: {
|
|||
|
||||
const deploymentOptions: ContractDeploymentOptions = {
|
||||
chain: options.chain,
|
||||
environment: options.environment,
|
||||
rpcUrl: finalRpcUrl,
|
||||
privateKey: options.privateKey,
|
||||
verified: options.verified,
|
||||
|
|
@ -209,13 +227,13 @@ export const deployContracts = async (options: {
|
|||
// Construct and execute deployment
|
||||
const deployCommand = constructDeployCommand(deploymentOptions);
|
||||
const env = buildDeploymentEnv(deploymentOptions);
|
||||
await executeDeployment(deployCommand, undefined, options.chain, env);
|
||||
await executeDeployment(deployCommand, undefined, networkId, env);
|
||||
|
||||
if (!txExecutionEnabled) {
|
||||
await emitOwnerTransactionCalldata(options.chain);
|
||||
await emitOwnerTransactionCalldata(networkId);
|
||||
}
|
||||
|
||||
logger.success(`DataHaven contracts deployed successfully to ${options.chain}`);
|
||||
logger.success(`DataHaven contracts deployed successfully to ${networkId}`);
|
||||
};
|
||||
|
||||
const normalizePrivateKey = (key?: string): `0x${string}` | undefined => {
|
||||
|
|
|
|||
|
|
@ -39,22 +39,27 @@ const DeploymentsSchema = z.object({
|
|||
|
||||
export type Deployments = z.infer<typeof DeploymentsSchema>;
|
||||
|
||||
export const parseDeploymentsFile = async (network = "anvil"): Promise<Deployments> => {
|
||||
const deploymentsPath = `../contracts/deployments/${network}.json`;
|
||||
/**
|
||||
* Parses the deployments file for a given network
|
||||
* @param networkId - The network identifier (e.g., "anvil", "hoodi", "stagenet-hoodi")
|
||||
* This can include an environment prefix like "stagenet-" or "testnet-"
|
||||
*/
|
||||
export const parseDeploymentsFile = async (networkId = "anvil"): Promise<Deployments> => {
|
||||
const deploymentsPath = `../contracts/deployments/${networkId}.json`;
|
||||
const deploymentsFile = Bun.file(deploymentsPath);
|
||||
if (!(await deploymentsFile.exists())) {
|
||||
logger.error(`File ${deploymentsPath} does not exist`);
|
||||
throw new Error(`Error reading ${network} deployments file`);
|
||||
throw new Error(`Error reading ${networkId} deployments file`);
|
||||
}
|
||||
const deploymentsJson = await deploymentsFile.json();
|
||||
logger.info(`Deployments: ${JSON.stringify(deploymentsJson, null, 2)}`);
|
||||
try {
|
||||
const parsedDeployments = DeploymentsSchema.parse(deploymentsJson);
|
||||
logger.debug(`Successfully parsed ${network} deployments file.`);
|
||||
logger.debug(`Successfully parsed ${networkId} deployments file.`);
|
||||
return parsedDeployments;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to parse ${network} deployments file:`, error);
|
||||
throw new Error(`Invalid ${network} deployments file format`);
|
||||
logger.error(`Failed to parse ${networkId} deployments file:`, error);
|
||||
throw new Error(`Invalid ${networkId} deployments file format`);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue