diff --git a/.env.example b/.env.example index 56ed6081..66a893b6 100644 --- a/.env.example +++ b/.env.example @@ -12,25 +12,31 @@ EXOCORE_GENESIS_PRIVATE_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3f USE_ENDPOINT_MOCK=true USE_EXOCORE_PRECOMPILE_MOCK=true -VALIDATOR_KEYS= -EXO_ADDRESSES= -NAMES= -CONS_KEYS= - -# The following are used by generate.js, in addition to CLIENT_CHAIN_RPC above. -BOOTSTRAP_ADDRESS= -EXCHANGE_RATES= -BASE_GENESIS_FILE_PATH= -RESULT_GENESIS_FILE_PATH= -BEACON_CHAIN_ENDPOINT= - # For contract verification ETHERSCAN_API_KEY= -# These are used for integration testing ETH PoS +# These are used for integration testing the Bootstrap contract, in addition to +# CLIENT_CHAIN_RPC and BEACON_CHAIN_ENDPOINT above. +INTEGRATION_VALIDATOR_KEYS=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80,0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d,0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a +INTEGRATION_STAKERS=0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6,0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a,0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba,0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e,0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356,0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97,0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 +INTEGRATION_TOKEN_DEPLOYERS=0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82,0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1 +INTEGRATION_CONTRACT_DEPLOYER=0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897 +# Specially ETH PoS related parameters. INTEGRATION_DEPOSIT_ADDRESS=0x6969696969696969696969696969696969696969 INTEGRATION_SECONDS_PER_SLOT=4 INTEGRATION_SLOTS_PER_EPOCH=3 INTEGRATION_BEACON_GENESIS_TIMESTAMP= INTEGRATION_DENEB_TIMESTAMP= -NST_DEPOSITOR= \ No newline at end of file +INTEGRATION_NST_DEPOSITOR=0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd +# margin tank lunch prison top episode peanut approve dish seat nominee illness +INTEGRATION_PUBKEY=0x98db81971df910a5d46314d21320f897060d76fdf137d22f0eb91a8693a4767d2a22730a3aaa955f07d13ad604f968e9 +INTEGRATION_SIGNATURE=0x922a316bdc3516bfa66e88259d5e93e339ef81bc85b70e6c715542222025a28fa1e3644c853beb8c3ba76a2c5c03b726081bf605bde3a16e1f33f902cc1b6c01093c19609de87da9383fa4b1f347bd2d4222e1ae5428727a7896c8e553cc8071 +# derived from pubkey + network params == chain id + genesis {fork version + validators root} +INTEGRATION_DEPOSIT_DATA_ROOT=0x456934ced8f08ff106857418a6d885ba69d31e1b7fab9a931be06da25490cd1d +INTEGRATION_BEACON_CHAIN_ENDPOINT=http://localhost:3500 +INTEGRATION_PROVE_ENDPOINT=http://localhost:8989 +# for generate.js +INTEGRATION_BOOTSTRAP_ADDRESS=0xF801fc13AA08876F343fEBf50dFfA52A78180811 +INTEGRATION_EXCHANGE_RATES=1000.123,2000.123,1799.345345 +INTEGRATION_BASE_GENESIS_FILE_PATH= +INTEGRATION_RESULT_GENESIS_FILE_PATH= diff --git a/package.json b/package.json index 0f8e600c..c68bc3b8 100644 --- a/package.json +++ b/package.json @@ -387,7 +387,6 @@ "scripts": { "test": "mocha" }, - "type": "module", "keywords": [], "author": "", "license": "ISC" diff --git a/script/generate.js b/script/generate.mjs similarity index 94% rename from script/generate.js rename to script/generate.mjs index 8ef2597b..552e1323 100644 --- a/script/generate.js +++ b/script/generate.mjs @@ -49,7 +49,7 @@ import dotenv from 'dotenv'; dotenv.config(); import { decode } from 'bech32'; import { promises as fs } from 'fs'; -import { Web3 } from 'web3'; +import Web3 from 'web3'; import Decimal from 'decimal.js'; import { getClient } from "@lodestar/api"; @@ -70,7 +70,14 @@ const isValidBech32 = (address) => { // Load variables from .env file -const { BEACON_CHAIN_ENDPOINT, CLIENT_CHAIN_RPC, BOOTSTRAP_ADDRESS, BASE_GENESIS_FILE_PATH, RESULT_GENESIS_FILE_PATH, EXCHANGE_RATES } = process.env; +const { + INTEGRATION_BEACON_CHAIN_ENDPOINT, + CLIENT_CHAIN_RPC, + INTEGRATION_BOOTSTRAP_ADDRESS, + INTEGRATION_BASE_GENESIS_FILE_PATH, + INTEGRATION_RESULT_GENESIS_FILE_PATH, + INTEGRATION_EXCHANGE_RATES +} = process.env; import pkg from 'js-sha3'; const { keccak256 } = pkg; @@ -106,9 +113,9 @@ async function updateGenesisFile() { const web3 = new Web3(CLIENT_CHAIN_RPC); // Create contract instance - const myContract = new web3.eth.Contract(contractABI, BOOTSTRAP_ADDRESS); + const myContract = new web3.eth.Contract(contractABI, INTEGRATION_BOOTSTRAP_ADDRESS); // Create beacon API client - const api = getClient({baseUrl: BEACON_CHAIN_ENDPOINT}, {config}); + const api = getClient({baseUrl: INTEGRATION_BEACON_CHAIN_ENDPOINT}, {config}); const spec = (await api.config.getSpec()).value(); const maxEffectiveBalance = new Decimal(spec.MAX_EFFECTIVE_BALANCE).mul(GWEI_TO_WEI); const ejectIonBalance = new Decimal(spec.EJECTION_BALANCE).mul(GWEI_TO_WEI); @@ -123,10 +130,10 @@ async function updateGenesisFile() { const stateRoot = web3.utils.bytesToHex(lastHeader.header.message.stateRoot); // Read exchange rates - const exchangeRates = EXCHANGE_RATES.split(',').map(Decimal); + const exchangeRates = INTEGRATION_EXCHANGE_RATES.split(',').map(Decimal); // Read the genesis file - const genesisData = await fs.readFile(BASE_GENESIS_FILE_PATH); + const genesisData = await fs.readFile(INTEGRATION_BASE_GENESIS_FILE_PATH); const genesisJSON = jsonBig.parse(genesisData); const height = parseInt(genesisJSON.initial_height, 10); @@ -207,7 +214,8 @@ async function updateGenesisFile() { const decimals = []; const supportedTokens = []; const assetIds = []; - const oracleTokens = {}; + // start with the initial value + const oracleTokens = genesisJSON.app_state.oracle.params.tokens; const oracleTokenFeeders = []; let offset = 0; for (let i = 0; i < supportedTokensCount; i++) { @@ -231,6 +239,7 @@ async function updateGenesisFile() { assetIds.push(token.tokenAddress.toLowerCase() + clientChainSuffix); const oracleToken = { name: tokenNamesForOracle[i], + index: offset, chain_id: 1, // constant intentionally, representing the first chain in the list (after the reserved blank one) contract_address: token.tokenAddress, active: true, @@ -238,7 +247,6 @@ async function updateGenesisFile() { decimal: 8, // price decimals, not token decimals } const oracleTokenFeeder = { - index: offset, token_id: (i + 1 - offset).toString(), // first is reserved rule_id: "1", start_round_id: "1", @@ -246,17 +254,17 @@ async function updateGenesisFile() { interval: "30", end_block: "0", } - if (oracleTokens.name in oracleTokens) { - oracleTokens[oracleTokens.name].asset_id += ',' + oracleToken.asset_id; + if (oracleToken.name in oracleTokens) { + oracleTokens[oracleToken.name].asset_id += ',' + oracleToken.asset_id; offset += 1; } else { - oracleTokens[oracleTokens.name] = oracleToken; + oracleTokens[oracleToken.name] = oracleToken; oracleTokenFeeders.push(oracleTokenFeeder); } // break; } genesisJSON.app_state.oracle.params.tokens = Object.values(oracleTokens) - .sort((a, b) => {a.index - b.uindex}) + .sort((a, b) => {a.index - b.index}) .map(({index, ...rest}) => rest); genesisJSON.app_state.oracle.params.token_feeders = oracleTokenFeeders; supportedTokens.sort((a, b) => { @@ -325,8 +333,6 @@ async function updateGenesisFile() { } totalEffectiveBalance = totalEffectiveBalance.plus(valEffectiveBalance); } - console.log(`Total effective balance for staker ${stakerAddress}: ${totalEffectiveBalance}`); - console.log(`Total deposit value for staker ${stakerAddress}: ${depositValue}`); if (depositValue > totalEffectiveBalance) { console.log("Staker has more deposit than effective balance."); // deposited 32 ETH and left with 31 ETH, aka downtime slashing @@ -348,11 +354,6 @@ async function updateGenesisFile() { continue; } } - let pendingSlashAmount = toSlash.sub(withdrawableValue); - if (pendingSlashAmount.gt(0)) { - withdrawableValue = withdrawableValue.minus(pendingSlashAmount); - depositValue = depositValue.minus(pendingSlashAmount); - } } else if (depositValue < totalEffectiveBalance) { // deposited 32 ETH and left with 33 ETH, aka rewards const delta = totalEffectiveBalance.minus(depositValue); @@ -363,16 +364,8 @@ async function updateGenesisFile() { staker_addr: stakerAddress.toLowerCase(), staker_index: staker_index_counter, validator_pubkey_list: pubKeys, - balance_list: [ - { - // TODO: check these values with Qing - round_id: 0, - block: height, - index: 0, - change: 0, - balance: depositValue.toString(), - } - ] + // the balance list represents the history of the balance. for bootstrap, that is empty. + balance_list: [] }); staker_index_counter += 1; } @@ -380,8 +373,8 @@ async function updateGenesisFile() { asset_id: tokenAddress.toLowerCase() + clientChainSuffix, info: { // adjusted for slashing by ETH beacon chain - total_deposit_amount: depositValue.toString(), - withdrawable_amount: withdrawableValue.toString(), + total_deposit_amount: depositValue.toFixed(), + withdrawable_amount: withdrawableValue.toFixed(), pending_undelegation_amount: "0", } }; @@ -810,10 +803,10 @@ async function updateGenesisFile() { } } ]; - genesisJSON.app_state.oracle.staker_infos_assets = { + genesisJSON.app_state.oracle.staker_infos_assets = [{ asset_id: VIRTUAL_STAKED_ETH_ADDR.toLowerCase() + clientChainSuffix, staker_infos: staker_infos, - }; + }]; // add the native chain and at the end so that count-related issues don't arise. genesisJSON.app_state.assets.client_chains.push(nativeChain); @@ -824,7 +817,10 @@ async function updateGenesisFile() { nativeAsset.asset_basic_info.layer_zero_chain_id.toString(16) ); - await fs.writeFile(RESULT_GENESIS_FILE_PATH, jsonBig.stringify(genesisJSON, null, 2)); + await fs.writeFile( + INTEGRATION_RESULT_GENESIS_FILE_PATH, + jsonBig.stringify(genesisJSON, null, 2) + ); console.log('Genesis file updated successfully.'); } catch (error) { console.error('Error updating genesis file:', error.message); diff --git a/script/integration/1_DeployBootstrap.s.sol b/script/integration/1_DeployBootstrap.s.sol index 4f1aa1ce..1cd803f8 100644 --- a/script/integration/1_DeployBootstrap.s.sol +++ b/script/integration/1_DeployBootstrap.s.sol @@ -47,19 +47,12 @@ contract DeployContracts is Script { // neither is the ownership of the contract being tested here address exocoreValidatorSet = vm.addr(uint256(0x8)); - // assumes 3 validators, to add more - change registerValidators and delegate. uint256[] validators; uint256[] stakers; + string[] exos; uint256 contractDeployer; uint256 nstDepositor; Bootstrap bootstrap; - // to add more tokens, - // 0. add deployer private keys - // 1. update the decimals - // 2. increase the size of MyToken - // 3. add information about tokens to deployTokens. - // 4. update deposit and delegate amounts in fundAndApprove and delegate. - // everywhere else we use the length of the myTokens array. uint256[] tokenDeployers; uint8[2] decimals = [18, 6]; address[] whitelistTokens; @@ -82,6 +75,9 @@ contract DeployContracts is Script { uint64 secondsPerSlot; uint64 slotsPerEpoch; uint256 beaconGenesisTimestamp; + bytes pubkey; + bytes signature; + bytes32 depositDataRoot; function setUp() private { // placate the pre-simulation runner @@ -106,15 +102,21 @@ contract DeployContracts is Script { ANVIL_TOKEN_DEPLOYERS[0] = uint256(0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82); ANVIL_TOKEN_DEPLOYERS[1] = uint256(0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1); - uint256 CONTRACT_DEPLOYER = uint256(0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897); + uint256 ANVIL_CONTRACT_DEPLOYER = uint256(0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897); - uint256 NST_DEPOSITOR = uint256(0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd); + uint256 ANVIL_NST_DEPOSITOR = uint256(0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd); - validators = vm.envOr("ANVIL_VALIDATORS", ",", ANVIL_VALIDATORS); - stakers = vm.envOr("ANVIL_STAKERS", ",", ANVIL_STAKERS); - tokenDeployers = vm.envOr("ANVIL_TOKEN_DEPLOYERS", ",", ANVIL_TOKEN_DEPLOYERS); - contractDeployer = vm.envOr("CONTRACT_DEPLOYER", CONTRACT_DEPLOYER); - nstDepositor = vm.envOr("NST_DEPOSITOR", NST_DEPOSITOR); + // load the keys for validators, stakers, token deployers, and the contract deployer + validators = vm.envOr("INTEGRATION_VALIDATOR_KEYS", ",", ANVIL_VALIDATORS); + // we don't validate the contents of the keys because vm.addr will throw if they are invalid + require(validators.length == 3, "Modify this script to support validators.length other than 3"); + stakers = vm.envOr("INTEGRATION_STAKERS", ",", ANVIL_STAKERS); + require(stakers.length == 7, "Modify this script to support stakers.length other than 7"); + tokenDeployers = vm.envOr("INTEGRATION_TOKEN_DEPLOYERS", ",", ANVIL_TOKEN_DEPLOYERS); + require(tokenDeployers.length == 2, "Modify this script to support tokenDeployers.length other than 2"); + require(decimals.length == tokenDeployers.length, "Decimals and tokenDeployers must have the same length"); + contractDeployer = vm.envOr("INTEGRATION_CONTRACT_DEPLOYER", ANVIL_CONTRACT_DEPLOYER); + nstDepositor = vm.envOr("INTEGRATION_NST_DEPOSITOR", ANVIL_NST_DEPOSITOR); // read the network configuration parameters and validate them depositAddress = vm.envOr("INTEGRATION_DEPOSIT_ADDRESS", address(0x6969696969696969696969696969696969696969)); @@ -131,6 +133,13 @@ contract DeployContracts is Script { require(slotsPerEpoch_ > 0, "Slots per epoch must be set"); require(slotsPerEpoch_ <= type(uint64).max, "Slots per epoch must be less than or equal to uint64 max"); slotsPerEpoch = uint64(slotsPerEpoch_); + // then, the Ethereum-native validator configuration + pubkey = vm.envBytes("INTEGRATION_PUBKEY"); + require(pubkey.length == 48, "Pubkey must be 48 bytes"); + signature = vm.envBytes("INTEGRATION_SIGNATURE"); + require(signature.length == 96, "Signature must be 96 bytes"); + depositDataRoot = vm.envBytes32("INTEGRATION_DEPOSIT_DATA_ROOT"); + require(depositDataRoot != bytes32(0), "Deposit data root must be set"); } function deployTokens() private { @@ -255,20 +264,11 @@ contract DeployContracts is Script { myAddress = bootstrap.createExoCapsule(); } console.log("ExoCapsule address", myAddress); - bootstrap.stake{value: 32 ether}( - // mnemonic: margin tank lunch prison top episode peanut approve dish seat nominee illness - hex"98db81971df910a5d46314d21320f897060d76fdf137d22f0eb91a8693a4767d2a22730a3aaa955f07d13ad604f968e9", // pubkey - hex"922a316bdc3516bfa66e88259d5e93e339ef81bc85b70e6c715542222025a28fa1e3644c853beb8c3ba76a2c5c03b726081bf605bde3a16e1f33f902cc1b6c01093c19609de87da9383fa4b1f347bd2d4222e1ae5428727a7896c8e553cc8071", // signature - bytes32(0x456934ced8f08ff106857418a6d885ba69d31e1b7fab9a931be06da25490cd1d) // deposit data root - ); + bootstrap.stake{value: 32 ether}(pubkey, signature, depositDataRoot); vm.stopBroadcast(); } function registerValidators() private { - // the mnemonics corresponding to the consensus public keys are given here. to recover, - // echo "${MNEMONIC}" | exocored init localnet --chain-id exocorelocal_233-1 --recover - // the value in this script is this one - // exocored keys consensus-pubkey-to-bytes --output json | jq -r .bytes string[3] memory exos = [ // these addresses will accrue rewards but they are not needed to keep the chain // running. @@ -277,6 +277,10 @@ contract DeployContracts is Script { "exo1rtg0cgw94ep744epyvanc0wdd5kedwql73vlmr" ]; string[3] memory names = ["validator1", "validator2", "validator3"]; + // the mnemonics corresponding to the consensus public keys are given here. to recover, + // echo "${MNEMONIC}" | exocored init localnet --chain-id exocorelocal_233-1 --recover + // the value in this script is this one + // exocored keys consensus-pubkey-to-bytes --output json | jq -r .bytes bytes32[3] memory pubKeys = [ // wonder quality resource ketchup occur stadium vicious output situate plug second // monkey harbor vanish then myself primary feed earth story real soccer shove like diff --git a/script/integration/2_VerifyDepositNST.s.sol b/script/integration/2_VerifyDepositNST.s.sol index 01191c6c..9ec9c15f 100644 --- a/script/integration/2_VerifyDepositNST.s.sol +++ b/script/integration/2_VerifyDepositNST.s.sol @@ -27,25 +27,28 @@ contract VerifyDepositNST is Script { uint256 nstDepositor; function setUp() public virtual { - // vm.chainId(ALLOWED_CHAIN_ID); // obtain the address string memory deployments = vm.readFile("script/integration/deployments.json"); bootstrapAddress = deployments.readAddress(".bootstrapAddress"); require(bootstrapAddress != address(0), "Bootstrap address not found"); beaconOracleAddress = deployments.readAddress(".beaconOracleAddress"); require(beaconOracleAddress != address(0), "BeaconOracle address not found"); - nstDepositor = - vm.envOr("NST_DEPOSITOR", uint256(0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd)); + nstDepositor = vm.envOr( + "INTEGRATION_NST_DEPOSITOR", uint256(0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd) + ); + require(nstDepositor != 0, "INTEGRATION_NST_DEPOSITOR not set"); } function run() external { bytes32[] memory validatorContainer; vm.startBroadcast(nstDepositor); Bootstrap bootstrap = Bootstrap(bootstrapAddress); + require(vm.exists("script/integration/proof.json"), "Proof file not found"); string memory data = vm.readFile("script/integration/proof.json"); // load the validator container validatorContainer = data.readBytes32Array(".validatorContainer"); // load the validator proof + // we don't validate it; that task is left to the contract. it is a test, after all. validatorProof = BeaconChainProofs.ValidatorContainerProof({ stateRoot: data.readBytes32(".stateRoot"), stateRootProof: data.readBytes32Array(".stateRootProof"), @@ -58,9 +61,13 @@ contract VerifyDepositNST is Script { oracle.addTimestamp(validatorProof.beaconBlockTimestamp); // now, the transactions bootstrap.verifyAndDepositNativeStake(validatorContainer, validatorProof); - // delegate only a small portion of the deposit for our test bootstrap.delegateTo( - "exo1rtg0cgw94ep744epyvanc0wdd5kedwql73vlmr", address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), 18 ether + // a validator in 1_DeployBootstrap.s.sol + "exo1rtg0cgw94ep744epyvanc0wdd5kedwql73vlmr", + // the native token address + address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), + // delegate only a small portion of the deposit for our test + 18 ether ); vm.stopBroadcast(); } diff --git a/script/integration/BeaconOracle.sol b/script/integration/BeaconOracle.sol index c74cb96d..78a08555 100644 --- a/script/integration/BeaconOracle.sol +++ b/script/integration/BeaconOracle.sol @@ -51,6 +51,9 @@ contract BeaconOracle is IBeaconChainOracle { } function addTimestamp(uint256 _targetTimestamp) external { + if (_targetTimestamp < GENESIS_BLOCK_TIMESTAMP) { + revert InvalidBlockTimestamp(); + } // If the targetTimestamp is not guaranteed to be within the beacon block root ring buffer, revert. if ((block.timestamp - _targetTimestamp) >= (BEACON_ROOTS_HISTORY_BUFFER_LENGTH * SECONDS_PER_SLOT)) { revert TimestampOutOfRange(); diff --git a/script/integration/deposit.sh b/script/integration/deposit.sh index bc8f2458..3fd23518 100755 --- a/script/integration/deposit.sh +++ b/script/integration/deposit.sh @@ -1,38 +1,94 @@ #!/usr/bin/env bash +set -e + # Get the directory of the script SCRIPT_DIR=$(dirname "$(readlink -f "$0")") +# Check that all of the variables required exist. +vars=( + CLIENT_CHAIN_RPC + INTEGRATION_BEACON_CHAIN_ENDPOINT + INTEGRATION_CONTRACT_DEPLOYER + INTEGRATION_DEPOSIT_DATA_ROOT + INTEGRATION_NST_DEPOSITOR + INTEGRATION_PUBKEY + INTEGRATION_SIGNATURE + INTEGRATION_STAKERS + INTEGRATION_TOKEN_DEPLOYERS + INTEGRATION_VALIDATOR_KEYS +) +for var in "${vars[@]}"; do + if [ -z "${!var}" ]; then + echo "Error: $var must be set" + exit 1 + fi +done + # Fetch the validator details and save them to container.json -curl -s -X GET "http://localhost:3500/eth/v1/beacon/genesis" -H "accept: application/json" | jq >"$SCRIPT_DIR/genesis.json" +if ! curl -s -X GET "$INTEGRATION_BEACON_CHAIN_ENDPOINT/eth/v1/beacon/genesis" -H "accept: application/json" | jq . >"$SCRIPT_DIR/genesis.json"; then + echo "Error: Failed to fetch genesis data from the beacon chain" + exit 1 +fi + +if ! jq -e .data "$SCRIPT_DIR/genesis.json" >/dev/null; then + echo "Error: Invalid genesis data structure." + exit 1 +fi # Fetch the spec sheet and save it to spec.json -curl -s http://localhost:3500/eth/v1/config/spec | jq >"$SCRIPT_DIR/spec.json" +if ! curl -s -X GET "$INTEGRATION_BEACON_CHAIN_ENDPOINT/eth/v1/config/spec" | jq >"$SCRIPT_DIR/spec.json"; then + echo "Error: Failed to fetch spec data from the beacon chain" + exit 1 +fi -# Ensure the request was successful -if [ $? -ne 0 ]; then - echo "Error: Failed to fetch genesis data of the beacon chain." +if ! jq -e .data "$SCRIPT_DIR/spec.json" >/dev/null; then + echo "Error: Invalid spec data structure." exit 1 fi timestamp=$(jq -r .data.genesis_time "$SCRIPT_DIR/genesis.json") -private_key=${NST_DEPOSITOR:-"0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd"} +private_key=$INTEGRATION_NST_DEPOSITOR sender=$(cast wallet a $private_key) +if [ $? -ne 0 ]; then + echo "Error: Failed to derive sender address." + exit 1 +fi deposit_address=$(jq -r .data.DEPOSIT_CONTRACT_ADDRESS "$SCRIPT_DIR/genesis.json") slots_per_epoch=$(jq -r .data.SLOTS_PER_EPOCH "$SCRIPT_DIR/spec.json") +if ! [[ "$slots_per_epoch" =~ ^[0-9]+$ ]]; then + echo "Error: Invalid slots per epoch" + exit 1 +fi seconds_per_slot=$(jq -r .data.SECONDS_PER_SLOT "$SCRIPT_DIR/spec.json") +if ! [[ "$seconds_per_slot" =~ ^[0-9]+$ ]]; then + echo "Error: Invalid slots per epoch" + exit 1 +fi -# Since the devnet uses `--fork=deneb` and `DENEB_FORK_EPOCH: 0`, the deneb time is equal to the beacon genesis time +# Make the variables available to the forge script +export INTEGRATION_VALIDATOR_KEYS=$INTEGRATION_VALIDATOR_KEYS +export INTEGRATION_STAKERS=$INTEGRATION_STAKERS +export INTEGRATION_TOKEN_DEPLOYERS=$INTEGRATION_TOKEN_DEPLOYERS +export INTEGRATION_CONTRACT_DEPLOYER=$INTEGRATION_CONTRACT_DEPLOYER +export INTEGRATION_PUBKEY=$INTEGRATION_PUBKEY +export INTEGRATION_SIGNATURE=$INTEGRATION_SIGNATURE +export INTEGRATION_DEPOSIT_DATA_ROOT=$INTEGRATION_DEPOSIT_DATA_ROOT export INTEGRATION_BEACON_GENESIS_TIMESTAMP=$timestamp +# Since the devnet uses `--fork=deneb` and `DENEB_FORK_EPOCH: 0`, the deneb time is equal to the beacon genesis time export INTEGRATION_DENEB_TIMESTAMP=$timestamp export INTEGRATION_SECONDS_PER_SLOT=$seconds_per_slot export INTEGRATION_SLOTS_PER_EPOCH=$slots_per_epoch export INTEGRATION_DEPOSIT_ADDRESS=$deposit_address +export INTEGRATION_NST_DEPOSITOR=$INTEGRATION_NST_DEPOSITOR export SENDER=$sender +# Specify SENDER so that libraries can be deployed +# Use Cancun version because prove.sh needs it or it complains +# Better to recompile here than to recompile in prove.sh forge script \ --skip-simulation script/integration/1_DeployBootstrap.s.sol \ --rpc-url $CLIENT_CHAIN_RPC \ - --broadcast -vvvv \ + --broadcast -v \ --sender $SENDER \ --evm-version cancun diff --git a/script/integration/prove.sh b/script/integration/prove.sh index 5f02b510..d1e6f84e 100755 --- a/script/integration/prove.sh +++ b/script/integration/prove.sh @@ -1,10 +1,41 @@ #!/usr/bin/env bash +set -e + # Get the directory of the script SCRIPT_DIR=$(dirname "$(readlink -f "$0")") +# Check that all of the variables required exist. +vars=( + CLIENT_CHAIN_RPC + INTEGRATION_BEACON_CHAIN_ENDPOINT + INTEGRATION_NST_DEPOSITOR + INTEGRATION_PROVE_ENDPOINT + INTEGRATION_PUBKEY +) +for var in "${vars[@]}"; do + if [ -z "${!var}" ]; then + echo "Error: $var must be set" + exit 1 + fi +done + +# Check for the files to exist +if [ ! -f "$SCRIPT_DIR/spec.json" ]; then + echo "Error: spec.json not found in $SCRIPT_DIR" + exit 1 +fi + +# Check for the files to exist +if [ ! -f "$SCRIPT_DIR/container.json" ]; then + echo "Error: container.json not found in $SCRIPT_DIR" + exit 1 +fi + # Fetch the validator details and save them to container.json -curl -s -X GET "http://localhost:3500/eth/v1/beacon/states/head/validators/0x98db81971df910a5d46314d21320f897060d76fdf137d22f0eb91a8693a4767d2a22730a3aaa955f07d13ad604f968e9" -H "accept: application/json" | jq >"$SCRIPT_DIR/container.json" +curl -s -X GET \ + "$INTEGRATION_BEACON_CHAIN_ENDPOINT/eth/v1/beacon/states/head/validators/$INTEGRATION_PUBKEY" \ + -H "accept: application/json" | jq >"$SCRIPT_DIR/container.json" # Ensure the request was successful if [ $? -ne 0 ]; then @@ -34,22 +65,29 @@ fi # Calculate the slot number slot=$((slots_per_epoch * epoch)) -# # Wait till the slot is reached -# seconds_per_slot=$(jq -r .data.SECONDS_PER_SLOT "$SCRIPT_DIR/spec.json") -# while true; do -# current_slot=$(curl http://localhost:3500/eth/v1/beacon/headers | jq -r '.data[0].header.message.slot') -# if (( current_slot > slot )); then -# break -# fi -# echo "Waiting for slot $slot, current slot is $current_slot" -# sleep $seconds_per_slot -# done - # Now derive the proof using the proof generation binary, which must already be running configured to the localnet -curl -X POST -H "Content-Type: application/json" \ - -d "{\"slot\": $slot, \"validator_index\": $validator_index}" \ - http://localhost:8989/v1/validator-proof | jq >"$SCRIPT_DIR/proof.json" +echo $INTEGRATION_PROVE_ENDPOINT +response=$(curl -s -w "%{http_code}" -X POST -H "Content-Type: application/json" \ + -d "{\"slot\": $slot, \"validator_index\": $validator_index}" \ + $INTEGRATION_PROVE_ENDPOINT/v1/validator-proof) + +http_code=${response: -3} +body=${response:0:${#response}-3} + +if [ "$http_code" != "200" ]; then + echo "Error: Failed to generate proof. HTTP code: $http_code" + echo "Response: $body" + exit 1 +fi + +echo "$body" | jq . >"$SCRIPT_DIR/proof.json" + +if [ ! -s "$SCRIPT_DIR/proof.json" ]; then + echo "Error: Generated proof is empty" + exit 1 +fi +export INTEGRATION_NST_DEPOSITOR=$INTEGRATION_NST_DEPOSITOR forge script script/integration/2_VerifyDepositNST.s.sol --skip-simulation \ --rpc-url $CLIENT_CHAIN_RPC --broadcast \ --evm-version cancun # required, otherwise you get EvmError: NotActivated diff --git a/test/foundry/unit/NetworkConfig.t.sol b/test/foundry/unit/NetworkConfig.t.sol index 89affbf1..78e16623 100644 --- a/test/foundry/unit/NetworkConfig.t.sol +++ b/test/foundry/unit/NetworkConfig.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import {stdError} from "forge-std/StdError.sol"; import "forge-std/Test.sol"; import {NetworkConfig} from "script/integration/NetworkConfig.sol";