diff --git a/docs/rpc.md b/docs/rpc.md index 75687b0f6..04d725892 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -711,10 +711,11 @@ Fulu (`blocks/{slot}`) and gloas (`produceBlockV4`) do the same, and the The payload attestation pool is shared with P2P, which fills it from accepted gossip, so a block this node builds carries the votes its peers sent too. -`tooling/kurtosis-validator/network_params_ethlambda_beacon.yaml` points the -validator client at this node alone, so every block on that devnet is one this -node built; it also runs spamoor, whose blob traffic makes those blocks carry -blobs. +A `cl_type: ethlambda` participant in our ethereum-package fork +([`lambdaclass/ethereum-package`](https://github.com/lambdaclass/ethereum-package/tree/add-ethlambda), +branch `add-ethlambda`) runs an ethlambda validator client that, by default, +talks to this node alone, so every block its validators propose is one this +node built. ### Encoding diff --git a/tooling/kurtosis-validator/kurtosis.yml b/tooling/kurtosis-validator/kurtosis.yml deleted file mode 100644 index ca22df8d2..000000000 --- a/tooling/kurtosis-validator/kurtosis.yml +++ /dev/null @@ -1,4 +0,0 @@ -name: github.com/lambdaclass/ethlambda_private/tooling/kurtosis-validator -description: | - A local Ethereum devnet from ethpandaops/ethereum-package, plus the - `ethlambda validator` client signing for validators no other client holds. diff --git a/tooling/kurtosis-validator/main.star b/tooling/kurtosis-validator/main.star deleted file mode 100644 index 37aff9572..000000000 --- a/tooling/kurtosis-validator/main.star +++ /dev/null @@ -1,386 +0,0 @@ -# A devnet from ethpandaops/ethereum-package, plus `ethlambda validator`. -# -# ethereum-package has no ethlambda client type, so this wraps it: run the -# devnet exactly as the package would, then add the ethlambda validator client -# as one more service in the same enclave, pointed at the first participant's -# beacon node over the enclave network. -# -# # Whose keys the ethlambda validator signs with -# -# Keys that are in genesis but that no validator client in the devnet holds. -# `network_params.preregistered_validator_count` is set above the participants' -# total, so the genesis state registers the extra keys while ethereum-package -# generates keystores only for the participants' ranges. This package derives -# the tail range itself, from the same mnemonic, with the same tool. -# -# That exclusivity is not optional. The ethlambda validator client keeps no -# slashing-protection record, so a key held by it and by any other client would -# be signed twice every slot. -# -# # Which beacon node it talks to -# -# By default, the first participant's. With `ethlambda_beacon.enabled`, the -# package also runs `ethlambda beacon`, paired with a geth of its own over the -# Engine API, and hands the validator client both nodes, ethlambda's first: the -# client fails over per call, so everything ethlambda beacon serves goes through -# it. With `ethlambda_beacon.fallback: false` the participant's node is left -# out of the list entirely, and ethlambda beacon serves every duty alone. -# -# # Which validator client -# -# `ethlambda validator` by default. With `ethlambda_validator.client: -# lighthouse`, Lighthouse's validator client signs for the same keys instead, -# against the same beacon node list: that is how the Beacon API ethlambda -# beacon serves is checked against a client other than its own. -# `ethlambda_validator.doppelganger: true` turns on Lighthouse's doppelganger -# protection, which calls `/eth/v1/validator/liveness`. - -ethereum_package = import_module("github.com/ethpandaops/ethereum-package/main.star") - -# ethereum-package's own default, so the derived keys match its genesis unless -# the args override the mnemonic. -DEFAULT_MNEMONIC = "giant issue aisle success illegal bike spike question tent bar rely arctic volcano long crawl hungry vocal artwork sniff fantasy very lucky have athlete" - -KEYS_ARTIFACT = "ethlambda-validator-keys" -KEYS_MOUNT = "/keys" -METRICS_PORT = 5064 - -# What ethereum-package names the genesis files and the Engine API secret, and -# where its own clients mount them. -GENESIS_ARTIFACT = "el_cl_genesis_data" -GENESIS_MOUNT = "/network-configs" -JWT_ARTIFACT = "jwt_file" -JWT_MOUNT = "/jwt" - -BEACON_API_PORT = 5052 -BEACON_METRICS_PORT = 5054 -BEACON_P2P_PORT = 9001 -BEACON_DISCOVERY_PORT = 9000 -ENGINE_PORT = 8551 - - -def run(plan, args={}): - vc = args.get("ethlambda_validator", {}) - devnet_args = { - k: v for k, v in args.items() if k not in ("ethlambda_validator", "ethlambda_beacon") - } - - network_params = devnet_args.get("network_params", {}) - # One entry per participant ethereum-package will start, in its order: - # `count` expands one config entry into several identical participants. - keys_per_participant = [] - for participant in devnet_args.get("participants", []): - keys = participant.get( - "validator_count", network_params.get("num_validator_keys_per_node", 128) - ) - keys_per_participant += [keys] * participant.get("count", 1) - participants_total = 0 - for keys in keys_per_participant: - participants_total += keys - genesis_total = network_params.get("preregistered_validator_count", 0) - if genesis_total <= participants_total: - fail( - ( - "network_params.preregistered_validator_count ({}) must exceed the " - + "participants' validators ({}), or there are no keys left for the " - + "ethlambda validator that another client does not already hold" - ).format(genesis_total, participants_total) - ) - - first = vc.get("first_index", participants_total) - last = vc.get("last_index", genesis_total) # exclusive - if first < participants_total or last > genesis_total or first >= last: - fail( - "ethlambda_validator keys [{}, {}) must lie within the unassigned range [{}, {})".format( - first, last, participants_total, genesis_total - ) - ) - - output = ethereum_package.run(plan, devnet_args) - beacon_nodes = [output.all_participants[0].cl_context.beacon_http_url] - - beacon = args.get("ethlambda_beacon", {}) - if beacon.get("enabled", False): - ethlambda_url = launch_ethlambda_beacon( - plan, - beacon, - output.all_participants[0].cl_context.enr, - ",".join([p.el_context.enode for p in output.all_participants]), - vc.get("image", "ghcr.io/lambdaclass/ethlambda:validator-local"), - ) - # With `fallback: false` the client talks to ethlambda beacon alone, so - # every duty, proposals included, has to go through it. - if beacon.get("fallback", True): - beacon_nodes = [ethlambda_url] + beacon_nodes - else: - beacon_nodes = [ethlambda_url] - name = vc.get("name", "ethlambda-vc") - if "dora" in devnet_args.get("additional_services", []): - label_in_dora(plan, first, last, name) - - mnemonic = network_params.get("preregistered_validator_keys_mnemonic", DEFAULT_MNEMONIC) - derive_keys(plan, mnemonic, first, last) - - if vc.get("client", "ethlambda") == "lighthouse": - launch_lighthouse_vc(plan, vc, beacon_nodes, name) - plan.print( - "Lighthouse's validator client signs for validators [{}, {}) via {}".format( - first, last, ", ".join(beacon_nodes) - ) - ) - return output - - cmd = [ - "validator", - "--beacon-nodes", - ",".join(beacon_nodes), - "--validators-dir", - KEYS_MOUNT + "/validators", - "--secrets-dir", - KEYS_MOUNT + "/raw/secrets", - "--http-address", - "0.0.0.0", - "--metrics-port", - str(METRICS_PORT), - "--graffiti", - vc.get("graffiti", name), - ] - fee_recipient = vc.get("suggested_fee_recipient", "") - if fee_recipient: - cmd += ["--suggested-fee-recipient", fee_recipient] - - plan.add_service( - name="vc-ethlambda", - config=ServiceConfig( - image=vc.get("image", "ghcr.io/lambdaclass/ethlambda:validator-local"), - cmd=cmd, - files={KEYS_MOUNT: KEYS_ARTIFACT}, - ports={ - "metrics": PortSpec( - number=METRICS_PORT, transport_protocol="TCP", application_protocol="http" - ), - }, - ), - ) - - plan.print( - "ethlambda validator signs for validators [{}, {}) via {}".format( - first, last, ", ".join(beacon_nodes) - ) - ) - return output - - -def launch_lighthouse_vc(plan, vc, beacon_nodes, name): - """Run Lighthouse's validator client on the derived keys. - - eth2-val-tools writes keys in Lighthouse's own layout - (`keys//voting-keystore.json` and `secrets/`), and with no - `validator_definitions.yml` in the validators directory Lighthouse - discovers them and writes one. It writes that file into the directory, so - the keys are copied out of the artifact first rather than used in place. - No `--datadir`: Lighthouse refuses it alongside `--validators-dir`, and - keeps its slashing-protection database in the validators directory, which - is the writable copy. `--init-slashing-protection` because that database - starts empty. - """ - flags = [ - "--testnet-dir={}".format(GENESIS_MOUNT), - "--beacon-nodes={}".format(",".join(beacon_nodes)), - "--validators-dir=/data/raw/keys", - "--secrets-dir=/data/raw/secrets", - "--init-slashing-protection", - "--graffiti={}".format(vc.get("graffiti", name)), - "--metrics", - "--metrics-address=0.0.0.0", - "--metrics-port={}".format(METRICS_PORT), - ] - fee_recipient = vc.get("suggested_fee_recipient", "") - if fee_recipient: - flags.append("--suggested-fee-recipient={}".format(fee_recipient)) - if vc.get("doppelganger", False): - flags.append("--enable-doppelganger-protection") - flags += vc.get("lighthouse_extra_params", []) - - plan.add_service( - name="vc-lighthouse", - config=ServiceConfig( - image=vc.get("lighthouse_image", "sigp/lighthouse:latest"), - entrypoint=["sh", "-c"], - cmd=[ - "mkdir -p /data && cp -r {}/raw /data/ && lighthouse vc {}".format( - KEYS_MOUNT, " ".join(flags) - ) - ], - files={KEYS_MOUNT: KEYS_ARTIFACT, GENESIS_MOUNT: GENESIS_ARTIFACT}, - ports={ - "metrics": PortSpec( - number=METRICS_PORT, transport_protocol="TCP", application_protocol="http" - ), - }, - ), - ) - - -def derive_keys(plan, mnemonic, first, last): - """Derive keystores for [first, last) and the definitions file the client reads. - - The same tool and flags ethereum-package uses for its own participants, so - these are the keys its genesis registered at those indices. `--insecure` - picks a cheap KDF, which matters on startup: the real one takes seconds per - key. - """ - # One line, with the commands chained by `&&`. Kurtosis passes the script - # through in a way that breaks a multi-line `for ... do ... done` loop (a - # launch failed with `Syntax error: ";" unexpected`), so nothing here may - # depend on a newline surviving. The printf format is single-quoted so its - # `\n` reaches printf rather than the shell. - definition = ( - "printf -- '- enabled: true\\n voting_public_key: \"%s\"\\n" - + " voting_keystore_path: {m}/raw/keys/%s/voting-keystore.json\\n" - + " voting_keystore_password_path: {m}/raw/secrets/%s\\n'" - + ' "$pubkey" "$pubkey" "$pubkey" >> /out/validators/validator_definitions.yml' - ).format(m=KEYS_MOUNT) - script = " && ".join( - [ - ( - "/app/eth2-val-tools keystores --insecure --prysm-pass unused --out-loc /out/raw" - + ' --source-mnemonic "{}" --source-min {} --source-max {}' - ).format(mnemonic, first, last), - "mkdir -p /out/validators", - 'for dir in /out/raw/keys/*; do pubkey=$(basename "$dir"); ' + definition + "; done", - 'echo "derived $(ls /out/raw/keys | wc -l) keys"', - ] - ) - - plan.run_sh( - name="ethlambda-validator-key-derivation", - description="Deriving the ethlambda validator's keys [{}, {})".format(first, last), - image="protolambda/eth2-val-tools:latest", - run=script, - store=[StoreSpec(src="/out", name=KEYS_ARTIFACT)], - ) - - -def label_in_dora(plan, first, last, name): - """Name the ethlambda validator's range in Dora. - - Dora labels validators from one file ethereum-package writes, listing only - its own participants, so without this our range shows as bare indices and - nothing on screen says ethlambda signed those blocks. Dora reads the file at - startup, so it is appended to and Dora restarted; a container restart keeps - its filesystem, so the edit survives. - """ - plan.exec( - service_name="dora", - description="Naming validators [{}, {}) {} in Dora".format(first, last, name), - recipe=ExecRecipe( - command=[ - "sh", - "-c", - "echo '{}-{}: {}' >> /validator-ranges/validator-ranges.yaml".format( - first, last - 1, name - ), - ] - ), - ) - plan.stop_service(name="dora", description="Restarting Dora to load the new name") - plan.start_service(name="dora", description="Restarting Dora to load the new name") - - -def launch_ethlambda_beacon(plan, beacon, bootnode_enr, el_enodes, default_image): - """Run `ethlambda beacon` with a geth of its own, and return its API URL. - - The geth starts from the same genesis as the devnet's. It still syncs - every block from ethlambda beacon, which hands it every payload in order - over the Engine API, so each one extends a parent geth already has. That - makes geth answer VALID rather than SYNCING, which keeps the node out of - optimistic mode, and the validator client refuses to sign against an - optimistic node. It peers with every participant's execution client (not - just the first) so transactions (spamoor's included) reach its mempool; - spamoor may submit to any of them, and a transaction that reaches only some - of them is not reliably relayed. Without that, the blocks this node - builds, the only blocks on this devnet, would always be empty. - - geth is started first because the Engine API client does not retry a block - once its attempts are spent. - """ - geth = plan.add_service( - name="el-ethlambda", - config=ServiceConfig( - image=beacon.get("geth_image", "ethereum/client-go:latest"), - entrypoint=["sh", "-c"], - cmd=[ - " ".join( - [ - "geth", - "--override.genesis={}/genesis.json".format(GENESIS_MOUNT), - "--datadir=/data/geth", - "--syncmode=full", - "--authrpc.addr=0.0.0.0", - "--authrpc.port={}".format(ENGINE_PORT), - "--authrpc.vhosts=*", - "--authrpc.jwtsecret={}/jwtsecret".format(JWT_MOUNT), - # The other ELs run discv5 only, so match them or the - # bootnode is never contacted. - "--discovery.v4=false", - "--discovery.v5=true", - "--bootnodes={}".format(el_enodes), - ] - ) - ], - files={GENESIS_MOUNT: GENESIS_ARTIFACT, JWT_MOUNT: JWT_ARTIFACT}, - ports={ - "engine": PortSpec(number=ENGINE_PORT, transport_protocol="TCP"), - }, - ), - ) - - bootnodes = plan.render_templates( - name="ethlambda-beacon-bootnodes", - config={"bootnodes.txt": struct(template="{{.enr}}\n", data={"enr": bootnode_enr})}, - ) - - service = plan.add_service( - name="cl-ethlambda", - config=ServiceConfig( - image=beacon.get("image", default_image), - cmd=[ - "beacon", - "--network", - GENESIS_MOUNT, - "--execution-endpoint", - "http://{}:{}".format(geth.ip_address, ENGINE_PORT), - "--execution-jwt-secret", - "{}/jwtsecret".format(JWT_MOUNT), - "--bootnodes", - "/bootnodes/bootnodes.txt", - "--data-dir", - "/data", - "--http-address", - "0.0.0.0", - "--api-port", - str(BEACON_API_PORT), - "--metrics-port", - str(BEACON_METRICS_PORT), - "--gossipsub-port", - str(BEACON_P2P_PORT), - "--discovery.port", - str(BEACON_DISCOVERY_PORT), - ], - files={ - GENESIS_MOUNT: GENESIS_ARTIFACT, - JWT_MOUNT: JWT_ARTIFACT, - "/bootnodes": bootnodes, - }, - ports={ - "http": PortSpec( - number=BEACON_API_PORT, transport_protocol="TCP", application_protocol="http" - ), - "metrics": PortSpec( - number=BEACON_METRICS_PORT, transport_protocol="TCP", application_protocol="http" - ), - }, - ), - ) - return "http://{}:{}".format(service.ip_address, BEACON_API_PORT) diff --git a/tooling/kurtosis-validator/network_params.yaml b/tooling/kurtosis-validator/network_params.yaml deleted file mode 100644 index fcd2abdff..000000000 --- a/tooling/kurtosis-validator/network_params.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# geth + lighthouse, with ethlambda as the only validator client. -# -# The lighthouse participant gets no keys, so ethereum-package registers all 128 -# genesis validators (`preregistered_validator_count`) without handing any of -# them to a client. The ethlambda validator derives and signs for all of them, -# and the keyless validator client ethereum-package still starts for lighthouse -# is removed. The chain therefore finalizes only if ethlambda works. -# -# Run from the repository root, after `make docker-build DOCKER_TAG=validator-local`: -# -# kurtosis run --enclave ethlambda-devnet tooling/kurtosis-validator \ -# --args-file tooling/kurtosis-validator/network_params.yaml -# -# Mainnet preset is mandatory, not a preference: the ethlambda binary is built -# with mainnet SSZ bounds and 32-slot epochs. -participants: - - el_type: geth - cl_type: lighthouse - validator_count: 0 - # Fulu needs a node custodying every data column. A node normally earns that - # from the validators attached to it, and this one has none. - supernode: true -network_params: - preset: mainnet - electra_fork_epoch: 0 - fulu_fork_epoch: 0 - preregistered_validator_count: 128 -additional_services: - - dora - -# Read by this package, not by ethereum-package. The key range is derived: every -# genesis validator no participant holds, which here is all of them. -ethlambda_validator: - image: ghcr.io/lambdaclass/ethlambda:validator-local - name: ethlambda-vc # shown in Dora, and used as the block graffiti - suggested_fee_recipient: "0x000000000000000000000000000000000000dEaD" diff --git a/tooling/kurtosis-validator/network_params_ethlambda_beacon.yaml b/tooling/kurtosis-validator/network_params_ethlambda_beacon.yaml deleted file mode 100644 index 29a1f6d1d..000000000 --- a/tooling/kurtosis-validator/network_params_ethlambda_beacon.yaml +++ /dev/null @@ -1,71 +0,0 @@ -# The devnet in network_params.yaml, with the ethlambda validator talking to -# `ethlambda beacon` first and the lighthouse participant second. -# -# ethlambda beacon runs with a geth of its own and serves the duties, -# attestation data, attestation pool, aggregation and block production, and the -# validator client talks to it alone (`fallback: false`), so every block on the -# chain is one ethlambda beacon built. Lighthouse stays in the network as a -# peer. Run from the repository root, after -# `make docker-build DOCKER_TAG=beacon-block-production`: -# -# kurtosis run --enclave ethlambda-beacon-devnet tooling/kurtosis-validator \ -# --args-file tooling/kurtosis-validator/network_params_ethlambda_beacon.yaml -# -# Two lighthouse participants rather than one, because ethereum-package pins a -# lone lighthouse to `--target-peers=0`, which makes it refuse every inbound -# peer, ethlambda beacon included, and lighthouse rejects the flag given twice, -# so it cannot be overridden. `--subscribe-all-subnets` because ethlambda beacon -# publishes attestations through gossipsub fanout, which reaches only peers -# subscribed to the subnet, and a lighthouse with no validators of its own joins -# almost none. ethlambda beacon aggregates for the validator client itself, so -# lighthouse needs no `--import-all-attestations`: it receives the aggregates on -# `beacon_aggregate_and_proof` and packs them into the blocks it proposes. -# -# `spamoor` with an explicit `blob-combined` spammer, so the blocks ethlambda -# beacon builds carry blobs, which exercises column publication and the cell -# proof checks. It is spelled out because `main.star` imports ethereum-package -# unpinned, so its default spammers may change. A user `spammers` list replaces -# the default list rather than appending to it (input_parser.star overwrites -# each `spamoor_params` key), so the default EOA spammer is dropped here. -# `shard_committee_period: 1` shortens how long a validator must have been -# active before it may exit, so a voluntary exit posted to the pool can be -# tested within minutes instead of 256 epochs. -participants: - - el_type: geth - cl_type: lighthouse - validator_count: 0 - supernode: true - count: 2 - cl_extra_params: - - --subscribe-all-subnets -network_params: - preset: mainnet - electra_fork_epoch: 0 - fulu_fork_epoch: 0 - preregistered_validator_count: 128 - shard_committee_period: 1 -additional_services: - - dora - - spamoor - -spamoor_params: - spammers: - - name: Blob Spammer - description: 3 type-4 blob transactions per slot with 1-2 sidecars each, gas/blobgas limit 20 gwei - scenario: blob-combined - config: - throughput: 6 - sidecars: 2 - max_pending: 6 - max_wallets: 20 - base_fee: 20 - blob_fee: 20 - -ethlambda_validator: - image: ghcr.io/lambdaclass/ethlambda:beacon-block-production - name: ethlambda-vc - suggested_fee_recipient: "0x000000000000000000000000000000000000dEaD" - -ethlambda_beacon: - enabled: true - fallback: false diff --git a/tooling/kurtosis-validator/network_params_lighthouse_vc.yaml b/tooling/kurtosis-validator/network_params_lighthouse_vc.yaml deleted file mode 100644 index 63b98cae5..000000000 --- a/tooling/kurtosis-validator/network_params_lighthouse_vc.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# network_params_ethlambda_beacon.yaml's devnet, with Lighthouse's validator -# client in place of ethlambda's, talking to `ethlambda beacon` alone, and with -# doppelganger protection on. -# -# This is how the Beacon API ethlambda beacon serves is checked against a -# validator client other than its own. In particular: -# - `/eth/v1/validator/liveness`: with doppelganger protection on, Lighthouse -# keeps its validators muted until liveness has answered `false` for them -# for a couple of epochs, and forever if the endpoint is missing. -# - `/eth/v1/validator/duties/sync`: with 128 validators and a 512-seat sync -# committee, every validator holds sync committee seats, so Lighthouse -# asks for sync duties from the first epoch. -# -# Run from the repository root, after -# `make docker-build DOCKER_TAG=beacon-api-lighthouse-defaults`: -# -# kurtosis run --enclave ethlambda-lighthouse-vc tooling/kurtosis-validator \ -# --args-file tooling/kurtosis-validator/network_params_lighthouse_vc.yaml -participants: - - el_type: geth - cl_type: lighthouse - validator_count: 0 - supernode: true - count: 2 - cl_extra_params: - - --subscribe-all-subnets -network_params: - preset: mainnet - electra_fork_epoch: 0 - fulu_fork_epoch: 0 - preregistered_validator_count: 128 -additional_services: - - dora - -ethlambda_validator: - client: lighthouse - doppelganger: true - # ethlambda beacon's image; the validator client is Lighthouse's. - image: ghcr.io/lambdaclass/ethlambda:beacon-api-lighthouse-defaults - name: lighthouse-vc - suggested_fee_recipient: "0x000000000000000000000000000000000000dEaD" - -ethlambda_beacon: - enabled: true - fallback: false