# How to create a merge testnet This information is compiled as of early Jan 22, a lot could change in the future. ## Generic Post-Merge information: A merge testnet has two components, a PoW testnet and a PoS testnet - the PoW testnet is then merged into the PoS testnet. The subsequent merge testnet will contain a Consensus Layer (CL) node and an Execution Layer(EL) node. ![](https://storage.googleapis.com/ethereum-hackmd/upload_88b310ae37f2c9373ac1e1f92e9ac1f7.png) Below are some general notes about merge testnets: * The CL node handles consensus, the EL node handles the state, transactions, RPC endpoint, and all the existing interfaces peopole use to interact with Ethereum. The information from the EL is placed as a "execution payload" in the block proposed by the CL. * A CL node depends on the EL node to propose slots, without an EL node the CL node will only be able to track the chain in an unverified state. A EL node depends on the CL node to get sync information (post merge). * A CL node refers to the eth2 beacon node. No functionality changes for the eth2 validator. The validator will continue to fetch information from the beacon node and attest/propose as it has done so far. * A validator connected to a CL node without an EL will fail to propose or attest blocks since it cannot verify any information about the payload. * This close coupling would mean its best to always have 1 EL node for 1 CL node (1-1 mapping). While 1-many and many-1 might work, they are not recommended and are currently untested. * The EL node will contain an Engine API, this Engine API would interact with the CL node for merge related tasks (setting head, forming payload). Please ensure you take care of this API endpoint, the Engine API is best not exposed to the internet and ideally filtered at a loadbalancer level(for infra providers). * If possible, expose the engine API in a different port than the JSON RPC (User port), this functionality isn't fully working as of now (early jan 22). * It is still unclear how infra providers (infura et all) will handle the Engine API/providing it to users. So it is best that all validators run their own EL. * The PoW testnet will have increasing difficulty with each block. A "Total Terminal Difficulty"(TTD) is specified in its chainspec, mining stops when `Difficulty >= TTD`. The PoS testnet has a config parameter labelled `MERGE_FORK_EPOCH`, once that epoch number is reached the PoS testnet is ready for the merge. The sequence of events MUST be: PoS ready for merge THEN PoW ready for merge, i.e `MERGE_FORK_EPOCH` occurs before `TTD` is hit! ## Merge Testnet considerations: The PoS testnet requires a deposit contract to know which validators are valid. There are two ways to achieve this in our merge testnet: 1. Start the PoW testnet, deploy the deposit contract. Use the deposit contract address in the PoS genesis/config. 2. allocate the deposit contract in the PoW testnet `genesis.json`, i.e it will exist at PoW testnet genesis (similar to pre-funded addresses). Usually `2.` is preferred since it allows us to start both testnets simultaneously and automate things. Similarly, there are two ways to define validators in the PoS testnet: 1. Manually perform deposits to the PoW testnet deposit contract. 2. Embed the deposits in the `genesis.ssz`(genesis state). This is akin to "pretending" the deposits were made and allows us to have a large validator set without having to spend testnet ether and time. Usuall `2.` is preferred since it allows us to have larger validator sizes and automatically run both testnets paralelly. The rest of this guide assumes option `2.` in both of the above cases. PoS testnets start wiht a `genesis.ssz` state, this state can be of type `phase0`, `altair` or `merge`. This describes the fork version from which the network should start. While it may be useful to start the network directly at the `merge` state, it is relatively untested to start from a random state. For the sake of interoperability, we will always generate a `phase0` state and configure a altair and merge fork epoch. ## Steps to setup merge testnet: We have a tool called the `ethereum-genesis-generator` that packages all the tools needed to create genesis files. The required configs are specified in a defined format and the tool generates the required genesis files. The tool then exposes a webserver port from which the config files can be fetched. This is to just make the experience easier for spinning up new testnets. The tool can be found [here](https://github.com/skylenet/ethereum-genesis-generator). The rest of this guide assumes you are using the above mentioned tool. An explanation of all the variables specified exists in [below](https://notes.ethereum.org/cmyGUbKVTTqhUGDg_GYThg#Merge-testnet-specific-variables). - [ ] Clone the `ethereum-genesis-generator` repo, create a folder and copy the contents of `config-example` into your folder PoW testnet: - [ ] Open the `<dir>/el/genesis-config.yaml` file - [ ] Generate a mnemonic (e.g with `eth2-val-tools mnemonic`) and enter the mnemonic - [ ] Choose unique chainID - [ ] Choose a `deposit contract address` - [ ] Choose a `TTD` (described ) - [ ] Choose a `mergeForkBlock` and `terminalTotalDifficulty` - [ ] If running with clique, then configure signers. If not set `enabled: false`. Ethereum Mainnet uses ethash, Goerli uses clique. When in doubt, set it to `false` - [ ] Run the `ethereum-genesis-generator` tool with the `el` command to generate the PoW genesis files. Save these folders locally. - [ ] Start bootnode (a simple geth node will do), get `enode` for peering - [ ] Start miner with `--mine --miner.threads=1 --miner.etherbase=<eth1 address>`(single CPU threaded miner). Nethermind doesn't support ethash, so we need to use geth. - [ ] Run other nodes with bootnode `enode`, check peering and block progression - [ ] Monitor `TTD` value by checking the latest block (e.g with the `eth_getBlockByNumber` RPC call) PoS testnet: - [ ] move into the folder with your configs, Open `<dir>/cl/config.yaml` - [ ] Decide date and time for CL genesis (You need the UNIX epoch time, (e.g with [epochconverter](https://www.epochconverter.com/)). Current version of the tool just uses `time.Now()` and sets that as the genesis time. Use the `CL_TIMESTAMP_DELAY_SECONDS` to get around this (tells the network to wait for the specified time to have genesis, but to continue with peering). e.g, `wanted_genesis_time` = `time.Now()` + `CL_TIMESTAMP_DELAY_SECONDS` - [ ] Decide the number of validators and enter it in `MIN_GENESIS_ACTIVE_VALIDATOR_COUNT` - [ ] Choose a unique `GENESIS_FORK_VERSION` - [ ] Choose a unique `ALTAIR_FORK_VERSION` (e.g just add 1 to the `GENESIS_FORK_VERSION`) - [ ] Choose a `ALTAIR_FORK_EPOCH` - [ ] Choose a unique `MERGE_FORK_VERSION` - [ ] Choose a `MERGE_FORK_EPOCH` - [ ] Enter the `TTD` value from the PoW testnet in the field `TERMINAL_TOTAL_DIFFICULTY`. Info on `TERMINAL_BLOCK_HASH` in [below](https://notes.ethereum.org/cmyGUbKVTTqhUGDg_GYThg#Merge-testnet-specific-variables) - [ ] Enter the chainID of the PoW testnet in the field `DEPOSIT_CHAIN_ID` and `DEPOSIT_NETWORK_ID` - [ ] Enter the `deposit contract address` specified in the PoW testnet genesis process - [ ] Open the `<dir>/cl/mnemonics.yaml` file and specify the mnemonics there, sum of all the `count` fields should be equal to `MIN_GENESIS_ACTIVE_VALIDATOR_COUNT`. You can use `eth2-val-tools` (e.g with `eth2-val-tools mnemonic`) to generate fresh mnemonics. More info [here](https://notes.ethereum.org/cmyGUbKVTTqhUGDg_GYThg#Merge-testnet-specific-variables) - [ ] Run the `ethereum-genesis-generator` tool with the `cl` command to generate the PoS genesis files, Save these locally. - [ ] Use the genesis files to deploy 1st client or bootnode - [ ] Get the `enr` from the logs, this bootnode enr will be used for peering - [ ] Generate your validator keys, e.g with `eth2-val-tools` or `ethereal` - [ ] Deploy the nodes with bootnode `enr` to get them peering and the validator keys to have them validating - [ ] Monitor a nodes logs to check that the genesis state has been imported correctly and that genesis has occurred - [ ] Monitor the epoch transitions and participation rates, >66% participation is needed for finality Note: You can also configure all the `el/cl` files first and then run the `ethereum-genesis-generator` with the `all` command to generate all the genesis files at once. ### Merge testnet specific variables **This section aims to purely explain what the variables are, the `ethereum-genesis-generator` tool will place them in the right files** The "merge" is controlled by the following variables(many of the values used in Kintsugi are used as examples). An example file has been linked where needed to help understand what the final outcome should look like. (Template `genesis.json` can be found [here](https://github.com/eth-clients/merge-testnets/blob/main/kintsugi/genesis.json), Info for nethermind specified in note below) For the PoW testnet: * A unique `chainId`/`networkID`, this is used for finding peers and reusing widespread(1,5..) values will lead to peering issues. Refer to [this](https://chainid.network/chains_mini.json) link for a compilation of popular chainIDs. This is of course not important if the testnet is purely local. * `terminalTotalDifficulty` in the `genesis.json`(chainspec). The TTD should be sufficiently high so as to not trigger the PoW merge readiness too early (remember, it needs to happen after the `MERGE_FORK_EPOCH`). The rate of increase of Difficulty (i.e, how quickly TTD is hit) depends on how many miners are running and how fast the miner is. A short test might be worth it to find a value that works for you. In general, a higher `TTD` value cannot hurt, you can always add more miners to speed it up. e.g: A single threaded CPU miner on a Digital Ocean instance (Premium AMD) will hit a `TTD` of `5000000000` in about ~16h. * `mergeForkBlock` in the `genesis.json`(chainspec). `mergeForkBlock` related to some peering changes in the EL. In a small scale, closed testnet - since everyone is on the same fork and peered with each other the `mergeForkBlock` doesn't play a big role. In a public testnet(e.g shadow fork), if `mergeForkBlock` is set too early, then the nodes with the modified `genesis.json` will not be peered with the "canonical" nodes. If all the block producers are in the "canonical" `genesis.json`, then the shadow fork will have no block production and TTD will never be hit. So to avoid such scenarios, set `mergeForkBlock` such that it would be hit after TTD on public testnets. i.e, TTD is hit, merge happens and then we un-peer from everyone who isn't on the merge fork. e.g `mergeForkBlock:100` and `TTD:180`, on Goerli the difficulty in each block is ~2, so TTD is hit on block ~90 and the un-peering happens at block 100, since post merge we will not rely on the old block producers - this works fine. * The PoS deposit contract address with its`code` and `storage` being specified in the `alloc`(allocations) section of the `genesis.json`. For an example of how this should look, refer to [this](https://github.com/eth-clients/merge-testnets/blob/main/kintsugi/genesis.json#L786). The `code` and `storage` doesn't change per testnet, You can always reuse the same values and change the `address` field as needed. Note: Geth/Besu require values in decimal, whereas nethermind requires them in Hex(with `0x` prefix). An example nethermind config can be found [here](https://github.com/eth-clients/merge-testnets/blob/main/kintsugi/nethermind_genesis.json). Additionally Nethermind specifies the TTD in the CLI parameters when starting the EL (`--Merge.TerminalTotalDifficulty`)(as of early jan 22). (Template `config.yaml` can be found [here](https://github.com/eth-clients/merge-testnets/blob/main/kintsugi/config.yaml)) For the PoS testnet: * `MIN_GENESIS_TIME` = time when nodes can start peering and can get ready for genesis * `GENESIS_DELAY` = added to `MIN_GENESIS_TIME`, to get the resulting genesis_time in the state. This is the delay allowed for the network to get ready. * `genesis_time` = The actual moment the network is considered live and epoch 0 starts. * A unique `GENESIS_FORK_VERSION`, this is used for finding peers. Reusing values will lead to peering issues. * `MIN_GENESIS_ACTIVE_VALIDATOR_COUNT` refers to the number of validators needed for genesis to occur. Ideally set this to 64 minimum, since we need 1 validator per slot and the validator is set 1 epoch in advance (so 2 epochs worth of validators are unique). It should be fine with a lower number, but 64 is the minimum recommended for the mainnet spec. * `ALTAIR_FORK_EPOCH` specifies the epoch at which the PoS testnet will go through the altair hardfork, e.g, Kintsugi uses `ALTAIR_FORK_EPOCH: 10` to specify that altair happens at the 10th epoch. * The `MERGE_FORK_EPOCH` specifies the epoch at which the PoS testnet will be merge ready. e.g, Kintusig uses `MERGE_FORK_EPOCH: 20` to specify that the PoS testnet is merge ready after the 20th epoch. Re-iterating, the `MERGE_FORK_EPOCH` must be set such that it happens before `TTD` is hit on the PoW testnet, avoid choosing a really high number. * The `MERGE_FORK_VERSION` specifies the fork id to be used after the merge, this is required for peering and should be unique. e.g, Kintusig uses `MERGE_FORK_VERSION: 0x62000071`. * `TERMINAL_TOTAL_DIFFICULTY`, this value should be the same as that specified in `genesis.json`. * `TERMINAL_BLOCK_HASH` specifies a block hash to use as a the terminal PoW block. This is for edge cases, and can be set to `0x0000000000000000000000000000000000000000000000000000000000000000` when not needed. * `TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH` to specify from when the `TERMINAL_BLOCK_HASH` can be used, needed for edge cases and can be set to `18446744073709551615` when not needed. * The `mnemonics.yaml` specifies the mnemonics from which the genesis validators are derived from. The format of the file is defined below. The sum of the `count` fields should be equal to `MIN_GENESIS_ACTIVE_VALIDATOR_COUNT` in order to start the network in a simple manner. The `yaml` file allows for multiple mnemonics purely for organization/sharing purposes. You can of course use a single mnemonic for all validators: ``` - mnemonic: "" # a 24 word BIP 39 mnemonic count: 1234 - mnemonic: "" # a 24 word BIP 39 mnemonic count: 1234 ``` ## Debugging ### General debugging advice: - Ensure the `--subscribe-all-subnets` or whatever variation of the flag exists in your CL client is enabled. This uses more resources, but being subscribed on all subnets helps reduce some peering related issues. ### Parsing genesis state This might be useful for a couple of reasons: * To debug an imporper genesis time or genesis delay * If the expected validators are too many or too few * If the client is complaining about the `genesis.ssz` * To verify that the `genesis.ssz` has been read correctly by the clients SSZ is a serialization format used on PoS (eth2) chains. The SSZ serialization/de-serialization requires a format, these formats are defined in the `consensus-specs` repo. It's also been provided as a python package we can import and use. Run `pip install eth2spec` to install the `eth2spec` package with the formats. Use the below python script to de-serialize the `genesis.ssz` file and parse its contents: ``` import os from eth2spec.phase0.mainnet import * with open('genesis.ssz', 'rb') as f: genesis_state = BeaconState.deserialize(f, os.stat('genesis.ssz').st_size) updated_header = genesis_state.latest_block_header.copy() updated_header.state_root = genesis_state.hash_tree_root() assert genesis_state.latest_block_header.state_root == Bytes32() print(f""" genesis_time: {genesis_state.genesis_time} genesis_state_root: 0x{genesis_state.hash_tree_root().hex()} genesis_latest_block_header: slot: {genesis_state.latest_block_header.slot} proposer_index: {genesis_state.latest_block_header.proposer_index} parent_root: 0x{genesis_state.latest_block_header.parent_root.hex()} state_root: 0x{genesis_state.latest_block_header.state_root.hex()} body_root: 0x{genesis_state.latest_block_header.body_root.hex()} genesis_block_root_no_state_root: 0x{genesis_state.latest_block_header.hash_tree_root().hex()} genesis_block_root_updated_state_root: 0x{updated_header.hash_tree_root().hex()} genesis_validators_root: 0x{genesis_state.validators.hash_tree_root().hex()} genesis_validators_count: {genesis_state.validators.length()} genesis_active_validators_count: {len(get_active_validator_indices(genesis_state, Epoch(0)))} genesis_total_active_stake_gwei: {get_total_active_balance(genesis_state)} genesis_total_balance_gwei: {sum(genesis_state.balances.readonly_iter())} eth1_data: deposit_root: 0x{genesis_state.eth1_data.deposit_root.hex()} deposit_count: {genesis_state.eth1_data.deposit_count} block_hash: 0x{genesis_state.eth1_data.block_hash.hex()} deposit index: {genesis_state.eth1_deposit_index} genesis_fork_version: 0x{genesis_state.fork.current_version.hex()} genesis_fork_digest: 0x{compute_fork_digest(genesis_state.fork.current_version, genesis_state.validators.hash_tree_root()).hex()} pre_genesis_fork_digest: 0x{compute_fork_digest(genesis_state.fork.current_version, Root()).hex()} """) ``` The script assumes that the `genesis.ssz` file is located in the same folder as the script, modify the path as needed. The script output tells us the various fields in the `genesis.ssz` file. Interesting ones to note are: * `genesis_time` (This is when epoch 0 goes live) * `genesis_validators_root` * `genesis_fork_version` * `genesis_active_validators_count` (should be the same as configured during genesis config creation) SSH into the node with the same `genesis.ssz` and you can use the API to get the data it's read. Run the command `curl localhost:5052/eth/v1/beacon/genesis` to fetch the nodes view of the information. Ensure they are the same with the parsed `genesis.ssz`. ### Validator deposit related debugging Useful for a scenario in which you make a deposit and it isn't showing up on the beacon chain or explorer. Overview: * Check deposit commands first * Ensure fork version is correct (it needs to ALWAYS use the `genesis fork version`) * Check the eth1_follow_distance (did you wait long enough?) * Validate the deposit data * Check explorer to cross-reference deposit validation * Check validators recognized by CL node * Use a prysm/lighthouse beacon node to verify the deposit_count it is voting for Naturally each debugging case is different, but hopefully this description gives some ideas for your scenario. You can generate the deposit data either with `eth2-val-tools` or `ethdo`. Ensure the `--fork-version=` is set correctly (genesis fork version ALWAYS!). Write out the deposit_data into a file for easier debugging. The output should look like this: ``` {"account":"m/12381/3600/14/0/0","deposit_data_root":"...","pubkey":"...","signature":"...","value":32000000000,"version":1,"withdrawal_credentials":"..."} ``` When in doubt, use a tool like `wagyu` (`wagyu ethereum import-hd --mnemonic "..." --derivation "m/44'/60'/0'/0/0"` (replace with required account derivation path) to compare the `pubkey` field in the deposit_data. Check the `ETH1_FOLLOW_DISTANCE` field in the `config.yaml` file used in the testnet configuration directory. This value will tell you how long the PoS testnet "waits" before accepting a transaction from the deposit contract. This is done to prevent rogue deposits due to re-orgs and is usually set to be a high value. If we are checking for a deposit that occurred too recently (within `ETH1_FOLLOW_DISTANCE`), then we just need to wait till it's detected. The CL node API exposes all the information it has about validators. This includes validators in the queue or awaiting exit. Run this query `curl localhost:5052/eth/v1/beacon/states/head/validators/<your validator public key>` to check if your CL node knows about your validator and what status it has. To get the entire validator list (Warning its big!), run `curl localhost:5052/eth/v1/beacon/states/head/validators`. Another way of checking the CL node's view is to get the latest block and comparing the `deposit_count` field. You can do this with the following command: `curl -s http://localhost:4000/eth/v1/beacon/blocks/head | jq '.data.message.body.eth1_data'`. The resultant value or block hash might be useful for debugging, it will go up with new deposits. The head block however will just tell you the deposit information up till the head. Each validator votes on how many deposits it has seen in order to process new deposits, which happens once there is consensus on the number(>66%). Lighthouse has a endpoint that allows you to see its deposit cache, which shows the number it will vote on. Run this command to get the data: `curl localhost:5052/lighthouse/eth1/deposit_cache | jq '.data`. The deposit cache also updates once `ETH1_FOLLOW_DISTANCE` time has passed. ## Slashing tests **WARNING, IF DONE RIGHT, THIS WILL SLASH YOUR VALIDATOR! THIS IS PURELY FOR TESTNET USE!** Here's a great link for information on [slashing](https://www.adiasg.me/2020/03/31/casper-ffg-explainer.html). Basically: A validator cannot make two votes (S1, T1) and (S2, T2) such that either of these conditions hold: 1.Double Vote: height(T1) = height(T2), or 2.Surround Vote: height(S1) < height(S2) < height(T2) < height(T1) 1. Double Vote: Two votes for blocks at the same height, but different forks. 2. Surround Vote: Different target and different height votes. Here is are small python scripts to generate a vote with the conflicting condition and dump the json body in order to submit it. Edit the required information before running it. Surround slashing: ``` surround_slashing.py # pip install git+https://github.com/ethereum/consensus-specs@master#egg=eth2spec # pip install git+https://github.com/ethereum/eth2.0-deposit-cli@master#egg=eth2deposit from pathlib import Path from eth2spec.config import config_util from eth2spec.altair import mainnet as spec # load config, ignore unused config variables spec.config = spec.Configuration(**{k: v for k, v in config_util.load_config_file(Path("config.yaml")).items() if hasattr(spec.Configuration, k)}) from eth2spec.utils import bls from eth2deposit.key_handling.key_derivation import path as hd_path import json import os # Let's get a single account slashed MNEMONIC = os.getenv("MNEMONIC") PASSWORD = os.getenv("PASSWORD") # empty in our case ACCOUNT_INDEX = int(os.getenv("ACCOUNT_INDEX")) VALIDATOR_INDEX = int(os.getenv("VALIDATOR_INDEX")) # see network repo readme of the testnet, for prater: 0x043db0d9a83813551ee2f33450d23797757d430911a9320530ad8a0eabc43efb GENESIS_VALIDATORS_ROOT = spec.Root.from_obj(os.getenv('GENESIS_VALIDATORS_ROOT')) PATH = f"m/12381/3600/{ACCOUNT_INDEX}/0/0" target_secret_key = hd_path.mnemonic_and_path_to_key(mnemonic=MNEMONIC.strip(), path=PATH, password=PASSWORD) target_pubkey = bls.SkToPk(target_secret_key) print(target_pubkey) slashable_indices = [VALIDATOR_INDEX] # surround attester slashing case: # different target, different source # source1 < source 2 # target 1 > target 2 slot_1 = 2175330 slot_2 = 2175295 target_epoch_1 = (slot_1 // 32) source_epoch_1 = target_epoch_1 - 3 target_epoch_2 = (slot_2 // 32) source_epoch_2 = target_epoch_2 source_1 = spec.Checkpoint(epoch=source_epoch_1, root=b"\x10" * 32) target_1 = spec.Checkpoint(epoch=target_epoch_1, root=b"\xaa" * 32) source_2 = spec.Checkpoint(epoch=source_epoch_2, root=b"\x10" * 32) target_2 = spec.Checkpoint(epoch=source_epoch_2, root=b"\xbb" * 32) committee_index = 0 attestation_data_1 = spec.AttestationData( slot=slot_1, index=committee_index, beacon_block_root=b"\x42" * 32, source=source_1, target=target_1, ) attestation_data_2 = spec.AttestationData( slot=slot_2, index=committee_index, beacon_block_root=b"\x42" * 32, source=source_2, target=target_2, ) def sign_att(att_data: spec.AttestationData) -> spec.BLSSignature: domain = spec.compute_domain(spec.DOMAIN_BEACON_ATTESTER, fork_version=spec.config.ALTAIR_FORK_VERSION, genesis_validators_root=GENESIS_VALIDATORS_ROOT) signing_root = spec.compute_signing_root(att_data, domain) return bls.Sign(target_secret_key, signing_root) att_1 = spec.IndexedAttestation(attesting_indices=slashable_indices, data=attestation_data_1, signature=sign_att(attestation_data_1)) att_2 = spec.IndexedAttestation(attesting_indices=slashable_indices, data=attestation_data_2, signature=sign_att(attestation_data_2)) slashing = spec.AttesterSlashing(attestation_1=att_1, attestation_2=att_2) print(json.dumps(slashing.to_obj(), indent=" ")) ``` Double vote slashing: ``` double_slashing.py # pip install git+https://github.com/ethereum/consensus-specs@master#egg=eth2spec # pip install git+https://github.com/ethereum/eth2.0-deposit-cli@master#egg=eth2deposit from pathlib import Path from eth2spec.config import config_util from eth2spec.altair import mainnet as spec # load config, ignore unused config variables spec.config = spec.Configuration(**{k: v for k, v in config_util.load_config_file(Path("config.yaml")).items() if hasattr(spec.Configuration, k)}) from eth2spec.utils import bls from eth2deposit.key_handling.key_derivation import path as hd_path import json import os # Let's get a single account slashed MNEMONIC = os.getenv("MNEMONIC") PASSWORD = os.getenv("PASSWORD") # empty in our case ACCOUNT_INDEX = int(os.getenv("ACCOUNT_INDEX")) VALIDATOR_INDEX = int(os.getenv("VALIDATOR_INDEX")) # see network repo readme of the testnet, for prater: 0x043db0d9a83813551ee2f33450d23797757d430911a9320530ad8a0eabc43efb GENESIS_VALIDATORS_ROOT = spec.Root.from_obj(os.getenv('GENESIS_VALIDATORS_ROOT')) PATH = f"m/12381/3600/{ACCOUNT_INDEX}/0/0" target_secret_key = hd_path.mnemonic_and_path_to_key(mnemonic=MNEMONIC.strip(), path=PATH, password=PASSWORD) target_pubkey = bls.SkToPk(target_secret_key) slashable_indices = [VALIDATOR_INDEX] # double attester slashing case: # different target, same source slot = 2173894 target_epoch = (slot // 32) source_epoch = target_epoch - 2 source = spec.Checkpoint(epoch=source_epoch, root=b"\x10" * 32) target_1 = spec.Checkpoint(epoch=source_epoch, root=b"\xaa" * 32) target_2 = spec.Checkpoint(epoch=source_epoch, root=b"\xbb" * 32) committee_index = 0 attestation_data_1 = spec.AttestationData( slot=slot, index=committee_index, beacon_block_root=b"\x42" * 32, source=source, target=target_1, ) attestation_data_2 = spec.AttestationData( slot=slot, index=committee_index, beacon_block_root=b"\x42" * 32, source=source, target=target_2, ) def sign_att(att_data: spec.AttestationData) -> spec.BLSSignature: domain = spec.compute_domain(spec.DOMAIN_BEACON_ATTESTER, fork_version=spec.config.ALTAIR_FORK_VERSION, genesis_validators_root=GENESIS_VALIDATORS_ROOT) signing_root = spec.compute_signing_root(att_data, domain) return bls.Sign(target_secret_key, signing_root) att_1 = spec.IndexedAttestation(attesting_indices=slashable_indices, data=attestation_data_1, signature=sign_att(attestation_data_1)) att_2 = spec.IndexedAttestation(attesting_indices=slashable_indices, data=attestation_data_2, signature=sign_att(attestation_data_2)) slashing = spec.AttesterSlashing(attestation_1=att_1, attestation_2=att_2) print(json.dumps(slashing.to_obj(), indent=" ")) ``` You can now take the `json` dump information and submit it to a node, so the false vote get's propagated. This can be done with the following command: ``` curl -X POST "https://localhost:5052/eth/v1/beacon/pool/attester_slashings" -H "accept: */*" -H "Content-Type: application/json" -d "<enter json dump data here>" ``` ## Future sections: - Client specific information - describe available tooling: zcli, ethereal, ethdo