Skip to content

feat: reject gossip envelopes if payload does not match its block hash - #10490

Open
nazarhussain wants to merge 15 commits into
unstablefrom
nh/envelope-block-hash
Open

nazarhussain wants to merge 15 commits into
unstablefrom
nh/envelope-block-hash

Conversation

@nazarhussain

@nazarhussain nazarhussain commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Implements consensus-specs#5770. Gossip only checked payload.block_hash == bid.block_hash, so a builder could gossip other contents under the committed hash, take the seen entry and get the real envelope ignored.

Draft until #5770 is released.

Description

Envelope gossip checks, new ones marked +:

  block known, not seen, not finalized       IGNORE
  slot, builder index match block            REJECT
+ parent beacon block root == block parent
+ prev_randao, gas_limit == bid
  block_hash, requests root == bid
  load block post-state                      IGNORE if missing
+ parent_hash == state.latest_block_hash
+ timestamp == slot time
+ withdrawals == state expected withdrawals
+ no empty transactions
+ payload hashes to block_hash               INVALID_BLOCK_HASH
  signature
  • computeExecutionBlockHash: keccak256 of the RLP header with MPT roots for transactions and withdrawals and the EIP-7685 requests hash. Bellatrix to heze.
  • Empty transactions are trie deletions and don't change the hash, hence the separate check.
  • Hash runs before the signature, as in the spec. The signing root hashes every transaction, so the signature check costs more.
  • Native node:crypto keccak. @ethereumjs/rlp is already installed via discv5.
  • Metric lodestar_gossip_execution_payload_envelope_block_hash_seconds with a dashboard panel.
  • Mock EL returns real block hashes.
  • Spec gossip runner wired. valid* and ignore_duplicate are skipped until the pinned vectors get real hashes.

Verification

  • Hashes match 447 spec sanity/blocks payloads (bellatrix to fulu) and 1000 real blocks each from Sepolia, mainnet and Platåberget (see comments). One Sepolia envelope is a test fixture.
  • Mean hash time on the main thread (M3 Pro): 0.5 ms Sepolia, 1 ms mainnet, 1.7 ms Platåberget.

AI Assistance Disclosure

This PR was written primarily by Claude Code.

🤖 Generated with Claude Code

nazarhussain and others added 6 commits October 28, 2026 08:07
Add computeExecutionBlockHash to derive an execution payload's block hash
from its header fields (RLP + keccak + ordered MPT roots), bellatrix
through gloas. Needed for the envelope gossip block hash check in
consensus-specs#5770.

Move @ethereumjs/rlp to beacon-node dependencies at ^10.1.1, the version
discv5 already ships.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Payloads built by the mock EL now carry their actual execution block
hash instead of random bytes, so envelopes from it pass the upcoming
gossip block hash check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hash

Implements consensus-specs#5770: an execution payload envelope is
rejected when its payload does not hash to payload.block_hash. Without
this, a builder could gossip other contents under the committed hash and
get the real envelope ignored.

The check runs after the signature check so an unsigned envelope cannot
make us hash its payload.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Wire the gossip_execution_payload_envelope handler into the gossip spec
runner. Imported gloas blocks seed the envelope input cache, as the
gossip block handler does, and valid envelopes are attached to it so
duplicates are detected.

Skip the valid and ignore_duplicate vectors for now: their envelopes
carry a placeholder block hash, which the consensus-specs#5770 check
rejects.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Hash with node:crypto keccak-256 (OpenSSL >= 3.2, bundled with Node
24.22.1) instead of the JS implementation from ethereum-cryptography,
which beacon-node no longer depends on.

The block hash check runs on the main thread in the envelope gossip
path. Benchmarked block hash cost (noble -> native):

- 300 txs x 400 B, 64 KiB BAL:    9.8 ms -> 1.2 ms
- 1500 txs x 300 B, 512 KiB BAL:  57 ms  -> 5.9 ms
- 2000 txs x 2 KB, 4 MiB BAL:     373 ms -> 18.5 ms

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@nazarhussain nazarhussain changed the title feat: reject gossip envelopes whose payload does not match its block hash feat: reject gossip envelopes if payload does not match its block hash Oct 28, 2026
Comment on lines +62 to +67
/** Matched against `<handler>/<testCase>` */
const skippedGossipTests: RegExp[] = [
// Envelopes in these vectors carry a placeholder block hash, so the block hash check from
// consensus-specs#5770 rejects them. Unskip once the vectors are regenerated with real hashes.
/^gossip_execution_payload_envelope\/gossip_execution_payload_envelope__(valid.*|ignore_duplicate)$/,
];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should verify that spec vectors from ethereum/consensus-specs#5770

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All 21 gloas cases now pass on both mainnet and minimal, while the 7 remaining failures per preset are heze cases still using old pinned vectors with placeholder hashes.

import {MockedBeaconChain, getMockedBeaconChain} from "../../../mocks/mockedBeaconChain.js";
import {generateProtoBlock} from "../../../utils/typeGenerator.js";

describe("validateGossipExecutionPayloadEnvelope", () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure about these, we wanna avoid custom gossip handler tests if it's covered by spec tests

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the duplicate tests which are part of the specs vectors now.

Match the gossip block handler, which seeds the envelope input with the
node's sampled and custody columns.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Performance Alert ⚠️

Possible performance regression was detected for some benchmarks.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold.

Benchmark suite Current: cc1046e Previous: 122e5d5 Ratio
enrSubnets - fastDeserialize 64 bits 2.5890 us/op 195.00 ns/op 13.28
Full benchmark results
Benchmark suite Current: cc1046e Previous: 122e5d5 Ratio
getPubkeys - native cache - req 1000 vs - 250000 vc 515.38 us/op 807.66 us/op 0.64
getPubkeys - validatorsArr - req 1000 vs - 250000 vc 32.804 us/op 38.122 us/op 0.86
BLS verify - blst 713.88 us/op 896.97 us/op 0.80
BLS verifyMultipleSignatures 3 - blst 1.0195 ms/op 1.3712 ms/op 0.74
BLS verifyMultipleSignatures 8 - blst 1.7492 ms/op 2.2624 ms/op 0.77
BLS verifyMultipleSignatures 32 - blst 5.6543 ms/op 7.6231 ms/op 0.74
BLS verifyMultipleSignatures 64 - blst 10.263 ms/op 14.429 ms/op 0.71
BLS verifyMultipleSignatures 128 - blst 19.913 ms/op 27.490 ms/op 0.72
BLS deserializing 10000 signatures 486.32 ms/op 645.99 ms/op 0.75
BLS deserializing 100000 signatures 4.9290 s/op 6.5938 s/op 0.75
BLS verifyMultipleSignatures - same message - 3 - blst 751.50 us/op 988.54 us/op 0.76
BLS verifyMultipleSignatures - same message - 8 - blst 850.48 us/op 1.1180 ms/op 0.76
BLS verifyMultipleSignatures - same message - 32 - blst 1.3084 ms/op 1.7328 ms/op 0.76
BLS verifyMultipleSignatures - same message - 64 - blst 1.9231 ms/op 2.5962 ms/op 0.74
BLS verifyMultipleSignatures - same message - 128 - blst 3.1321 ms/op 4.3111 ms/op 0.73
BLS aggregatePubkeys 32 - blst 14.639 us/op 19.893 us/op 0.74
BLS aggregatePubkeys 128 - blst 52.294 us/op 71.412 us/op 0.73
getSlashingsAndExits - default max 32.584 us/op 55.346 us/op 0.59
getSlashingsAndExits - 2k 330.38 us/op 373.87 us/op 0.88
proposeBlockBody type=full, size=empty 1.2556 ms/op 676.11 us/op 1.86
isKnown best case - 1 super set check 120.00 ns/op 212.00 ns/op 0.57
isKnown normal case - 2 super set checks 120.00 ns/op 168.00 ns/op 0.71
isKnown worse case - 16 super set checks 123.00 ns/op 165.00 ns/op 0.75
validate api signedAggregateAndProof - struct 1.1135 ms/op 1.5714 ms/op 0.71
validate gossip signedAggregateAndProof - struct 1.1086 ms/op 1.5697 ms/op 0.71
batch validate gossip attestation - vc 640000 - chunk 32 99.415 us/op 127.42 us/op 0.78
batch validate gossip attestation - vc 640000 - chunk 64 85.607 us/op 107.04 us/op 0.80
batch validate gossip attestation - vc 640000 - chunk 128 70.495 us/op 96.537 us/op 0.73
batch validate gossip attestation - vc 640000 - chunk 256 78.628 us/op 98.612 us/op 0.80
block hash typical 300 txs x 400 B, BAL 64 KiB 1.8328 ms/op
transactions trie typical 300 txs x 400 B, BAL 64 KiB 1.5525 ms/op
block hash heavy 1500 txs x 300 B, BAL 512 KiB 10.827 ms/op
transactions trie heavy 1500 txs x 300 B, BAL 512 KiB 10.346 ms/op
block hash near gossip max 2000 txs x 2000 B, BAL 4096 KiB 17.068 ms/op
transactions trie near gossip max 2000 txs x 2000 B, BAL 4096 KiB 16.686 ms/op
bytes32 toHexString 243.00 ns/op 308.00 ns/op 0.79
bytes32 Buffer.toString(hex) 127.00 ns/op 168.00 ns/op 0.76
bytes32 Buffer.toString(hex) from Uint8Array 198.00 ns/op 242.00 ns/op 0.82
bytes32 Buffer.toString(hex) + 0x 135.00 ns/op 172.00 ns/op 0.78
Return object 10000 times 0.19850 ns/op 0.21440 ns/op 0.93
Throw Error 10000 times 1.5096 us/op 1.8064 us/op 0.84
toHex 71.679 ns/op 92.658 ns/op 0.77
Buffer.from 65.633 ns/op 84.934 ns/op 0.77
shared Buffer 43.777 ns/op 62.231 ns/op 0.70
fastMsgIdFn sha256 / 200 bytes 1.2130 us/op 1.4900 us/op 0.81
fastMsgIdFn h32 xxhash / 200 bytes 135.00 ns/op 163.00 ns/op 0.83
fastMsgIdFn h64 xxhash / 200 bytes 164.00 ns/op 228.00 ns/op 0.72
fastMsgIdFn sha256 / 1000 bytes 3.7870 us/op 4.8340 us/op 0.78
fastMsgIdFn h32 xxhash / 1000 bytes 200.00 ns/op 252.00 ns/op 0.79
fastMsgIdFn h64 xxhash / 1000 bytes 202.00 ns/op 258.00 ns/op 0.78
fastMsgIdFn sha256 / 10000 bytes 33.222 us/op 42.144 us/op 0.79
fastMsgIdFn h32 xxhash / 10000 bytes 965.00 ns/op 1.2970 us/op 0.74
fastMsgIdFn h64 xxhash / 10000 bytes 608.00 ns/op 863.00 ns/op 0.70
send data - 1000 256B messages 3.8550 ms/op 4.3122 ms/op 0.89
send data - 1000 512B messages 5.2164 ms/op 5.6396 ms/op 0.92
send data - 1000 1024B messages 5.7230 ms/op 6.3739 ms/op 0.90
send data - 1000 1200B messages 6.4311 ms/op 6.7465 ms/op 0.95
send data - 1000 2048B messages 10.384 ms/op 8.8112 ms/op 1.18
send data - 1000 4096B messages 39.413 ms/op 63.055 ms/op 0.63
send data - 1000 16384B messages 294.39 ms/op 443.43 ms/op 0.66
send data - 1000 65536B messages 1.7839 s/op 1.8176 s/op 0.98
enrSubnets - fastDeserialize 64 bits 2.5890 us/op 195.00 ns/op 13.28
enrSubnets - ssz BitVector 64 bits 193.00 ns/op 270.00 ns/op 0.71
enrSubnets - fastDeserialize 4 bits 70.000 ns/op 96.000 ns/op 0.73
enrSubnets - ssz BitVector 4 bits 197.00 ns/op 270.00 ns/op 0.73
prioritizePeers score -10:0 att 32-0.1 sync 2-0 168.97 us/op 203.55 us/op 0.83
prioritizePeers score 0:0 att 32-0.25 sync 2-0.25 184.33 us/op 230.22 us/op 0.80
prioritizePeers score 0:0 att 32-0.5 sync 2-0.5 259.84 us/op 321.85 us/op 0.81
prioritizePeers score 0:0 att 64-0.75 sync 4-0.75 462.58 us/op 570.72 us/op 0.81
prioritizePeers score 0:0 att 64-1 sync 4-1 553.18 us/op 689.68 us/op 0.80
archive index / 1 slot / archive root index lookup 12.736 us/op 22.026 us/op 0.58
archive index / 1 slot / archive block lookup, decode and hash 2.0822 ms/op 2.4668 ms/op 0.84
archive index / 1 slots / 1 columns / legacy 15.598 us/op 24.532 us/op 0.64
archive index / 1 slots / 1 columns / flat files 89.411 us/op 178.99 us/op 0.50
archive index / 1 slots / 8 columns / legacy 73.682 us/op 85.038 us/op 0.87
archive index / 1 slots / 8 columns / flat files 209.49 us/op 373.18 us/op 0.56
archive index / 1 slots / 128 columns / legacy 7.5265 ms/op 6.5972 ms/op 1.14
archive index / 1 slots / 128 columns / flat files 8.1023 ms/op 5.3466 ms/op 1.52
archive index / 8 slots / 1 columns / legacy 131.88 us/op 176.00 us/op 0.75
archive index / 8 slots / 1 columns / flat files 703.85 us/op 1.2968 ms/op 0.54
archive index / 8 slots / 8 columns / legacy 997.39 us/op 728.30 us/op 1.37
archive index / 8 slots / 8 columns / flat files 2.1851 ms/op 3.1389 ms/op 0.70
archive index / 8 slots / 128 columns / legacy 71.603 ms/op 101.89 ms/op 0.70
archive index / 8 slots / 128 columns / flat files 276.61 ms/op 253.17 ms/op 1.09
head state / 1 slot / archive root index lookup 13.602 us/op 21.614 us/op 0.63
head state / 1 slot / archive block lookup, decode and hash 2.0811 ms/op 2.5903 ms/op 0.80
head state / 1 slots / 1 columns / legacy 15.027 us/op 23.986 us/op 0.63
head state / 1 slots / 1 columns / flat files 82.652 us/op 142.31 us/op 0.58
head state / 1 slots / 8 columns / legacy 62.541 us/op 74.671 us/op 0.84
head state / 1 slots / 8 columns / flat files 222.94 us/op 348.67 us/op 0.64
head state / 1 slots / 128 columns / legacy 12.779 ms/op 4.4396 ms/op 2.88
head state / 1 slots / 128 columns / flat files 2.8921 ms/op 4.8143 ms/op 0.60
head state / 8 slots / 1 columns / legacy 111.23 us/op 188.30 us/op 0.59
head state / 8 slots / 1 columns / flat files 610.90 us/op 1.1064 ms/op 0.55
head state / 8 slots / 8 columns / legacy 1.9796 ms/op 601.94 us/op 3.29
head state / 8 slots / 8 columns / flat files 1.6425 ms/op 3.0103 ms/op 0.55
head state / 8 slots / 128 columns / legacy 99.439 ms/op 97.176 ms/op 1.02
head state / 8 slots / 128 columns / flat files 250.65 ms/op 206.57 ms/op 1.21
array of 16000 items push then shift 768.15 ns/op 1.2921 us/op 0.59
LinkedList of 16000 items push then shift 5.0100 ns/op 6.6760 ns/op 0.75
array of 16000 items push then pop 43.842 ns/op 77.721 ns/op 0.56
LinkedList of 16000 items push then pop 4.7370 ns/op 5.9110 ns/op 0.80
array of 24000 items push then shift 1.0970 us/op 1.9115 us/op 0.57
LinkedList of 24000 items push then shift 5.7700 ns/op 6.3430 ns/op 0.91
array of 24000 items push then pop 65.693 ns/op 106.69 ns/op 0.62
LinkedList of 24000 items push then pop 4.8270 ns/op 5.9030 ns/op 0.82
intersect bitArray bitLen 8 2.3980 ns/op 3.8720 ns/op 0.62
intersect array and set length 8 22.658 ns/op 31.489 ns/op 0.72
intersect bitArray bitLen 128 15.016 ns/op 23.097 ns/op 0.65
intersect array and set length 128 378.77 ns/op 519.19 ns/op 0.73
bitArray.getTrueBitIndexes() bitLen 128 854.00 ns/op 902.00 ns/op 0.95
bitArray.getTrueBitIndexes() bitLen 248 1.4900 us/op 1.6540 us/op 0.90
bitArray.getTrueBitIndexes() bitLen 512 3.2120 us/op 3.4260 us/op 0.94
Full columns - reconstruct all 6 blobs 136.59 us/op 134.47 us/op 1.02
Full columns - reconstruct half of the blobs out of 6 83.059 us/op 72.545 us/op 1.14
Full columns - reconstruct single blob out of 6 30.414 us/op 5.9686 ms/op 0.01
Half columns - reconstruct all 6 blobs 306.23 ms/op 390.98 ms/op 0.78
Half columns - reconstruct half of the blobs out of 6 155.30 ms/op 194.52 ms/op 0.80
Half columns - reconstruct single blob out of 6 54.827 ms/op 70.093 ms/op 0.78
Set add up to 64 items then delete first 1.3501 us/op 2.6432 us/op 0.51
OrderedSet add up to 64 items then delete first 2.0008 us/op 3.5339 us/op 0.57
Set add up to 64 items then delete last 1.6419 us/op 3.0994 us/op 0.53
OrderedSet add up to 64 items then delete last 2.6010 us/op 3.9720 us/op 0.65
Set add up to 64 items then delete middle 1.6131 us/op 3.0212 us/op 0.53
OrderedSet add up to 64 items then delete middle 3.3128 us/op 5.3388 us/op 0.62
Set add up to 128 items then delete first 4.0999 us/op 5.7509 us/op 0.71
OrderedSet add up to 128 items then delete first 6.2173 us/op 7.6103 us/op 0.82
Set add up to 128 items then delete last 4.0539 us/op 5.9917 us/op 0.68
OrderedSet add up to 128 items then delete last 5.8460 us/op 7.8187 us/op 0.75
Set add up to 128 items then delete middle 4.2849 us/op 5.7897 us/op 0.74
OrderedSet add up to 128 items then delete middle 9.8908 us/op 13.269 us/op 0.75
Set add up to 256 items then delete first 7.8519 us/op 11.570 us/op 0.68
OrderedSet add up to 256 items then delete first 12.624 us/op 15.578 us/op 0.81
Set add up to 256 items then delete last 7.6112 us/op 12.174 us/op 0.63
OrderedSet add up to 256 items then delete last 11.818 us/op 15.851 us/op 0.75
Set add up to 256 items then delete middle 8.2451 us/op 11.681 us/op 0.71
OrderedSet add up to 256 items then delete middle 29.572 us/op 38.261 us/op 0.77
runFastConfirmationRules vc:100000 bc:96 eq:0 2.8870 ms/op 3.9941 ms/op 0.72
runFastConfirmationRules vc:600000 bc:96 eq:0 26.654 ms/op 32.788 ms/op 0.81
runFastConfirmationRules vc:1000000 bc:96 eq:0 44.297 ms/op 60.566 ms/op 0.73
runFastConfirmationRules vc:600000 bc:320 eq:0 25.332 ms/op 32.018 ms/op 0.79
runFastConfirmationRules vc:100000 bc:96 eq:1000 1.0817 s/op 1.2482 s/op 0.87
pass gossip attestations to forkchoice per slot 3.5284 ms/op 4.6147 ms/op 0.76
forkChoice updateHead vc 100000 bc 64 eq 0 350.78 us/op 479.22 us/op 0.73
forkChoice updateHead vc 600000 bc 64 eq 0 1.8279 ms/op 2.9212 ms/op 0.63
forkChoice updateHead vc 1000000 bc 64 eq 0 3.0750 ms/op 4.7361 ms/op 0.65
forkChoice updateHead vc 600000 bc 320 eq 0 1.8496 ms/op 2.4321 ms/op 0.76
forkChoice updateHead vc 600000 bc 1200 eq 0 1.8756 ms/op 2.5159 ms/op 0.75
forkChoice updateHead vc 600000 bc 7200 eq 0 2.3682 ms/op 3.4465 ms/op 0.69
forkChoice updateHead vc 600000 bc 64 eq 1000 1.9246 ms/op 2.9391 ms/op 0.65
forkChoice updateHead vc 600000 bc 64 eq 10000 2.0464 ms/op 3.0761 ms/op 0.67
forkChoice updateHead vc 600000 bc 64 eq 300000 5.4823 ms/op 7.4317 ms/op 0.74
forkChoice updateHead vc 600000 bc 64 eq 0 gloas boosted 2.2398 ms/op 3.4101 ms/op 0.66
computeDeltas 1400000 validators 0% inactive 8.3714 ms/op 12.337 ms/op 0.68
computeDeltas 1400000 validators 10% inactive 7.9312 ms/op 11.865 ms/op 0.67
computeDeltas 1400000 validators 20% inactive 7.5264 ms/op 11.905 ms/op 0.63
computeDeltas 1400000 validators 50% inactive 6.1995 ms/op 9.7083 ms/op 0.64
computeDeltas 2100000 validators 0% inactive 11.345 ms/op 19.916 ms/op 0.57
computeDeltas 2100000 validators 10% inactive 10.368 ms/op 18.960 ms/op 0.55
computeDeltas 2100000 validators 20% inactive 9.6595 ms/op 17.900 ms/op 0.54
computeDeltas 2100000 validators 50% inactive 7.8227 ms/op 14.520 ms/op 0.54
altair processAttestation - 250000 vs - 7PWei normalcase 12.466 ms/op 6.2833 ms/op 1.98
altair processAttestation - 250000 vs - 7PWei worstcase 14.664 ms/op 9.1300 ms/op 1.61
altair processAttestation - setStatus - 1/6 committees join 71.406 us/op 122.92 us/op 0.58
altair processAttestation - setStatus - 1/3 committees join 142.06 us/op 243.12 us/op 0.58
altair processAttestation - setStatus - 1/2 committees join 196.45 us/op 321.72 us/op 0.61
altair processAttestation - setStatus - 2/3 committees join 257.87 us/op 392.17 us/op 0.66
altair processAttestation - setStatus - 4/5 committees join 361.01 us/op 593.55 us/op 0.61
altair processAttestation - setStatus - 100% committees join 436.47 us/op 674.04 us/op 0.65
altair processBlock - 250000 vs - 7PWei normalcase 13.826 ms/op 8.7666 ms/op 1.58
altair processBlock - 250000 vs - 7PWei normalcase hashState 35.270 ms/op 31.173 ms/op 1.13
altair processBlock - 250000 vs - 7PWei worstcase 33.238 ms/op 35.597 ms/op 0.93
altair processBlock - 250000 vs - 7PWei worstcase hashState 68.351 ms/op 60.647 ms/op 1.13
phase0 processBlock - 250000 vs - 7PWei normalcase 5.4957 ms/op 4.0078 ms/op 1.37
phase0 processBlock - 250000 vs - 7PWei worstcase 50.221 ms/op 39.028 ms/op 1.29
altair processEth1Data - 250000 vs - 7PWei normalcase 222.40 us/op 296.95 us/op 0.75
getExpectedWithdrawals 250000 eb:1,eth1:1,we:0,wn:0,smpl:16 2.3520 us/op 3.3450 us/op 0.70
getExpectedWithdrawals 250000 eb:0.95,eth1:0.1,we:0.05,wn:0,smpl:220 16.637 us/op 18.762 us/op 0.89
getExpectedWithdrawals 250000 eb:0.95,eth1:0.3,we:0.05,wn:0,smpl:43 4.3920 us/op 5.6810 us/op 0.77
getExpectedWithdrawals 250000 eb:0.95,eth1:0.7,we:0.05,wn:0,smpl:19 2.5320 us/op 3.6560 us/op 0.69
getExpectedWithdrawals 250000 eb:0.1,eth1:0.1,we:0,wn:0,smpl:1021 77.100 us/op 101.96 us/op 0.76
getExpectedWithdrawals 250000 eb:0.03,eth1:0.03,we:0,wn:0,smpl:11778 1.0425 ms/op 1.3593 ms/op 0.77
getExpectedWithdrawals 250000 eb:0.01,eth1:0.01,we:0,wn:0,smpl:16384 1.3967 ms/op 1.7587 ms/op 0.79
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,smpl:16384 1.5104 ms/op 1.8238 ms/op 0.83
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,nocache,smpl:16384 2.8099 ms/op 3.7509 ms/op 0.75
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,smpl:16384 1.5741 ms/op 2.0073 ms/op 0.78
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,nocache,smpl:16384 4.6769 ms/op 4.0238 ms/op 1.16
Tree 40 250000 create 287.68 ms/op 370.45 ms/op 0.78
Tree 40 250000 get(125000) 71.513 ns/op 106.14 ns/op 0.67
Tree 40 250000 set(125000) 786.22 ns/op 1.0760 us/op 0.73
Tree 40 250000 toArray() 19.497 ms/op 9.9694 ms/op 1.96
Tree 40 250000 iterate all - toArray() + loop 19.918 ms/op 16.210 ms/op 1.23
Tree 40 250000 iterate all - get(i) 36.470 ms/op 38.465 ms/op 0.95
Array 250000 create 1.5484 ms/op 2.4805 ms/op 0.62
Array 250000 clone - spread 361.11 us/op 792.78 us/op 0.46
Array 250000 get(125000) 0.26200 ns/op 0.30400 ns/op 0.86
Array 250000 set(125000) 0.26000 ns/op 0.30700 ns/op 0.85
Array 250000 iterate all - loop 46.567 us/op 56.410 us/op 0.83
phase0 afterProcessEpoch - 250000 vs - 7PWei 5.9023 ms/op 16.535 ms/op 0.36
Array.fill - length 1000000 2.3236 ms/op 4.0238 ms/op 0.58
Array push - length 1000000 9.2751 ms/op 15.555 ms/op 0.60
Array.get 0.18577 ns/op 0.21025 ns/op 0.88
Uint8Array.get 0.20347 ns/op 0.23905 ns/op 0.85
phase0 beforeProcessEpoch - 250000 vs - 7PWei 16.813 ms/op 24.778 ms/op 0.68
altair processEpoch - mainnet_e81889 235.87 ms/op 281.69 ms/op 0.84
mainnet_e81889 - altair beforeProcessEpoch 31.963 ms/op 38.651 ms/op 0.83
mainnet_e81889 - altair processJustificationAndFinalization 5.2370 us/op 6.5090 us/op 0.80
mainnet_e81889 - altair processInactivityUpdates 6.3066 ms/op 3.6030 ms/op 1.75
mainnet_e81889 - altair processRewardsAndPenalties 18.529 ms/op 23.980 ms/op 0.77
mainnet_e81889 - altair processRegistryUpdates 450.00 ns/op 621.00 ns/op 0.72
mainnet_e81889 - altair processSlashings 114.00 ns/op 143.00 ns/op 0.80
mainnet_e81889 - altair processEth1DataReset 113.00 ns/op 134.00 ns/op 0.84
mainnet_e81889 - altair processEffectiveBalanceUpdates 1.2640 ms/op 1.6432 ms/op 0.77
mainnet_e81889 - altair processSlashingsReset 586.00 ns/op 702.00 ns/op 0.83
mainnet_e81889 - altair processRandaoMixesReset 883.00 ns/op 1.0100 us/op 0.87
mainnet_e81889 - altair processHistoricalRootsUpdate 114.00 ns/op 138.00 ns/op 0.83
mainnet_e81889 - altair processParticipationFlagUpdates 390.00 ns/op 456.00 ns/op 0.86
mainnet_e81889 - altair processSyncCommitteeUpdates 90.000 ns/op 106.00 ns/op 0.85
mainnet_e81889 - altair afterProcessEpoch 42.732 ms/op 44.802 ms/op 0.95
capella processEpoch - mainnet_e217614 745.71 ms/op 886.95 ms/op 0.84
mainnet_e217614 - capella beforeProcessEpoch 73.487 ms/op 82.294 ms/op 0.89
mainnet_e217614 - capella processJustificationAndFinalization 5.8720 us/op 34.066 ms/op 0.00
mainnet_e217614 - capella processInactivityUpdates 14.039 ms/op 17.960 ms/op 0.78
mainnet_e217614 - capella processRewardsAndPenalties 91.555 ms/op 109.02 ms/op 0.84
mainnet_e217614 - capella processRegistryUpdates 3.6310 us/op 4.8940 us/op 0.74
mainnet_e217614 - capella processSlashings 112.00 ns/op 143.00 ns/op 0.78
mainnet_e217614 - capella processEth1DataReset 110.00 ns/op 135.00 ns/op 0.81
mainnet_e217614 - capella processEffectiveBalanceUpdates 10.948 ms/op 22.405 ms/op 0.49
mainnet_e217614 - capella processSlashingsReset 553.00 ns/op 910.00 ns/op 0.61
mainnet_e217614 - capella processRandaoMixesReset 959.00 ns/op 1.1610 us/op 0.83
mainnet_e217614 - capella processHistoricalSummariesUpdate 112.00 ns/op 185.00 ns/op 0.61
mainnet_e217614 - capella processParticipationFlagUpdates 366.00 ns/op 534.00 ns/op 0.69
mainnet_e217614 - capella afterProcessEpoch 29.293 ms/op 58.566 ms/op 0.50
phase0 processEpoch - mainnet_e58758 239.80 ms/op 252.93 ms/op 0.95
mainnet_e58758 - phase0 beforeProcessEpoch 69.701 ms/op 104.79 ms/op 0.67
mainnet_e58758 - phase0 processJustificationAndFinalization 5.2610 us/op 8.6700 us/op 0.61
mainnet_e58758 - phase0 processRewardsAndPenalties 15.670 ms/op 18.023 ms/op 0.87
mainnet_e58758 - phase0 processRegistryUpdates 1.8050 us/op 2.4910 us/op 0.72
mainnet_e58758 - phase0 processSlashings 110.00 ns/op 143.00 ns/op 0.77
mainnet_e58758 - phase0 processEth1DataReset 105.00 ns/op 137.00 ns/op 0.77
mainnet_e58758 - phase0 processEffectiveBalanceUpdates 517.55 us/op 855.50 us/op 0.60
mainnet_e58758 - phase0 processSlashingsReset 689.00 ns/op 1.1570 us/op 0.60
mainnet_e58758 - phase0 processRandaoMixesReset 984.00 ns/op 1.5090 us/op 0.65
mainnet_e58758 - phase0 processHistoricalRootsUpdate 112.00 ns/op 141.00 ns/op 0.79
mainnet_e58758 - phase0 processParticipationRecordUpdates 914.00 ns/op 1.3170 us/op 0.69
mainnet_e58758 - phase0 afterProcessEpoch 5.9956 ms/op 7.9249 ms/op 0.76
phase0 processEffectiveBalanceUpdates - 250000 normalcase 674.10 us/op 1.0496 ms/op 0.64
phase0 processEffectiveBalanceUpdates - 250000 worstcase 0.5 851.51 us/op 3.8619 ms/op 0.22
gloas processInactivityUpdates - 250000 inactivity leak all eligible missed target 10.016 ms/op 43.873 ms/op 0.23
phase0 processRegistryUpdates - 250000 normalcase 1.5890 us/op 2.9810 us/op 0.53
phase0 processRegistryUpdates - 250000 badcase_full_deposits 114.69 us/op 174.97 us/op 0.66
phase0 processRegistryUpdates - 250000 worstcase 0.5 56.239 ms/op 91.012 ms/op 0.62
altair processRewardsAndPenalties - 250000 normalcase 14.120 ms/op 84.986 ms/op 0.17
altair processRewardsAndPenalties - 250000 worstcase 13.113 ms/op 17.396 ms/op 0.75
phase0 getAttestationDeltas - 250000 normalcase 3.9156 ms/op 5.9101 ms/op 0.66
phase0 getAttestationDeltas - 250000 worstcase 3.3532 ms/op 6.0207 ms/op 0.56
phase0 processSlashings - 250000 worstcase 44.565 us/op 68.047 us/op 0.65
altair processSyncCommitteeUpdates - 250000 7.4809 ms/op 10.640 ms/op 0.70
BeaconState.hashTreeRoot - No change 130.00 ns/op 173.00 ns/op 0.75
BeaconState.hashTreeRoot - 1 full validator 86.061 us/op 117.22 us/op 0.73
BeaconState.hashTreeRoot - 32 full validator 1.2112 ms/op 1.4734 ms/op 0.82
BeaconState.hashTreeRoot - 512 full validator 10.750 ms/op 10.563 ms/op 1.02
BeaconState.hashTreeRoot - 1 validator.effectiveBalance 89.015 us/op 124.02 us/op 0.72
BeaconState.hashTreeRoot - 32 validator.effectiveBalance 1.4047 ms/op 2.3967 ms/op 0.59
BeaconState.hashTreeRoot - 512 validator.effectiveBalance 23.923 ms/op 35.370 ms/op 0.68
BeaconState.hashTreeRoot - 1 balances 83.271 us/op 109.09 us/op 0.76
BeaconState.hashTreeRoot - 32 balances 773.28 us/op 1.5003 ms/op 0.52
BeaconState.hashTreeRoot - 512 balances 7.3249 ms/op 10.064 ms/op 0.73
BeaconState.hashTreeRoot - 250000 balances 138.85 ms/op 217.01 ms/op 0.64
aggregationBits - 2048 els - zipIndexesInBitList 15.489 us/op 23.527 us/op 0.66
regular array get 100000 times 19.327 us/op 23.440 us/op 0.82
wrappedArray get 100000 times 19.760 us/op 23.564 us/op 0.84
arrayWithProxy get 100000 times 7.4165 ms/op 10.200 ms/op 0.73
ssz.Root.equals 15.453 ns/op 21.843 ns/op 0.71
byteArrayEquals 15.122 ns/op 21.649 ns/op 0.70
Buffer.compare 6.4640 ns/op 9.1460 ns/op 0.71
processSlot - 1 slots 8.4900 us/op 18.703 us/op 0.45
processSlot - 32 slots 1.9648 ms/op 2.8054 ms/op 0.70
getEffectiveBalanceIncrementsZeroInactive - 250000 vs - 7PWei 3.2629 ms/op 9.5095 ms/op 0.34
getCommitteeAssignments - req 1 vs - 250000 vc 1.4545 ms/op 1.7448 ms/op 0.83
getCommitteeAssignments - req 100 vs - 250000 vc 3.3838 ms/op 3.5833 ms/op 0.94
getCommitteeAssignments - req 1000 vs - 250000 vc 3.5395 ms/op 3.8709 ms/op 0.91
findModifiedValidators - 10000 modified validators 762.02 ms/op 1.2604 s/op 0.60
findModifiedValidators - 1000 modified validators 447.49 ms/op 782.76 ms/op 0.57
findModifiedValidators - 100 modified validators 311.58 ms/op 568.20 ms/op 0.55
findModifiedValidators - 10 modified validators 276.19 ms/op 517.19 ms/op 0.53
findModifiedValidators - 1 modified validators 242.32 ms/op 404.72 ms/op 0.60
findModifiedValidators - no difference 193.28 ms/op 396.56 ms/op 0.49
migrate state 1500000 validators, 3400 modified, 2000 new 3.1918 s/op 3.7216 s/op 0.86
RootCache.getBlockRootAtSlot - 250000 vs - 7PWei 2.5300 ns/op 3.3600 ns/op 0.75
state getBlockRootAtSlot - 250000 vs - 7PWei 750.37 ns/op 547.85 ns/op 1.37
computeProposerIndex 100000 validators 1.1084 ms/op 1.2162 ms/op 0.91
getNextSyncCommitteeIndices 1000 validators 135.10 us/op 145.35 us/op 0.93
getNextSyncCommitteeIndices 10000 validators 296.52 us/op 339.88 us/op 0.87
getNextSyncCommitteeIndices 100000 validators 1.9016 ms/op 2.2339 ms/op 0.85
computeProposers - vc 250000 438.19 us/op 452.49 us/op 0.97
computeEpochShuffling - vc 250000 13.298 ms/op 42.156 ms/op 0.32
getNextSyncCommittee - vc 250000 6.9349 ms/op 8.9338 ms/op 0.78
nodejs block root to RootHex using toHex 68.135 ns/op 89.094 ns/op 0.76
nodejs block root to RootHex using toRootHex 42.744 ns/op 58.070 ns/op 0.74
nodejs fromHex(blob) 922.95 us/op 1.0247 ms/op 0.90
nodejs fromHexInto(blob) 53.332 us/op 69.465 us/op 0.77
nodejs block root to RootHex using the deprecated toHexString 503.05 ns/op 587.72 ns/op 0.86
nodejs byteArrayEquals 32 bytes (block root) 17.340 ns/op 24.727 ns/op 0.70
nodejs byteArrayEquals 48 bytes (pubkey) 24.918 ns/op 35.965 ns/op 0.69
nodejs byteArrayEquals 96 bytes (signature) 25.442 ns/op 33.363 ns/op 0.76
nodejs byteArrayEquals 1024 bytes 30.658 ns/op 39.767 ns/op 0.77
nodejs byteArrayEquals 131072 bytes (blob) 942.13 ns/op 1.6772 us/op 0.56
browser block root to RootHex using toHex 108.13 ns/op 137.01 ns/op 0.79
browser block root to RootHex using toRootHex 101.52 ns/op 123.70 ns/op 0.82
browser fromHex(blob) 1.5239 ms/op 1.9428 ms/op 0.78
browser fromHexInto(blob) 434.96 us/op 626.00 us/op 0.69
browser block root to RootHex using the deprecated toHexString 501.54 ns/op 419.99 ns/op 1.19
browser byteArrayEquals 32 bytes (block root) 18.066 ns/op 27.519 ns/op 0.66
browser byteArrayEquals 48 bytes (pubkey) 25.866 ns/op 38.845 ns/op 0.67
browser byteArrayEquals 96 bytes (signature) 48.425 ns/op 73.018 ns/op 0.66
browser byteArrayEquals 1024 bytes 497.00 ns/op 739.23 ns/op 0.67
browser byteArrayEquals 131072 bytes (blob) 62.327 us/op 93.210 us/op 0.67

by benchmarkbot/action

@nazarhussain

nazarhussain commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Validated and benchmarked computeExecutionBlockHash against real Sepolia gloas data.

Validation

  • 1000 consecutive finalized gloas envelopes (slots 11451288 to 11452288), all with transactions, fetched from a public Sepolia beacon API.
  • Computed hash matches payload.block_hash for all 1000, 0 mismatches. These are real EL hashes with real block access lists.
  • 25 blocks carry execution requests, every block has 16 withdrawals, so the requests hash and withdrawals root are covered too.

Also ran the gossip_execution_payload_envelope vectors regenerated with real block hashes (gloas, mainnet and minimal): all 21 cases pass, including all valid*, ignore_duplicate and the new reject_invalid_block_hash. The skip list stays until a spec release ships these vectors for gloas and heze.

Sepolia block shape

Per block mean p50 p99 max
Transactions 97 97 150 186
Transaction data 58 KiB 55 KiB 120 KiB 190 KiB
Block access list 41 KiB 39 KiB 88 KiB 141 KiB

Benchmark on the same payloads (ms per block)

Measured on Apple M3 Pro (11 cores, 18 GiB), macOS 27.2, Node v24.22.1 (OpenSSL 3.5.9), single thread, mean of 5 runs per block.

mean p50 p90 p99 max
Block hash, native keccak (this PR) 0.48 0.47 0.62 0.88 1.34
Block hash, ethereum-cryptography keccak 4.72 4.61 6.73 8.94 13.46
Transactions trie only 0.35 0.34 0.46 0.71 1.48
BAL keccak, native 0.06 0.06 0.08 0.13 0.22
BAL keccak, ethereum-cryptography 1.62 1.55 2.32 3.42 5.85

Native keccak is about 10x faster on real blocks. The check adds about 0.5 ms per envelope to gossip validation, under 1.4 ms in the worst block. The transactions trie is about 73% of the remaining cost.

@wemeetagain

Copy link
Copy Markdown
Member

Sepolia block shape

those numbers look low. we should try to see what it looks like using 200m gas. Maybe plataberget has different numbers

@nazarhussain

Copy link
Copy Markdown
Contributor Author

🤖 Same check on mainnet, which has larger blocks than Sepolia.

1000 consecutive finalized mainnet blocks (fulu, slots 15527190 to 15528192): computed hash matches block_hash for all 1000.

Blocks: 238 txs and 166 KiB tx data on average, up to 2574 txs and 805 KiB (Sepolia: 97 txs, 58 KiB).

ms per block mean p99 max
Native keccak (this PR) 1.01 4.54 7.88
ethereum-cryptography keccak 8.22 22.00 32.25

The transactions trie is ~92% of the cost; the max is the 2574-tx block. Mainnet has no BAL yet; native keccak adds ~1.4 ms per MiB of BAL.

Apple M3 Pro, Node v24.22.1 (OpenSSL 3.5.9), single thread, mean of 5 runs per block.

@wemeetagain

Copy link
Copy Markdown
Member

Maybe not worth optimizing, but also curious what napi bound computeExecutionBlockHash numbers look like (probably passing serialized payload instead of deserialized payload).

@wemeetagain

Copy link
Copy Markdown
Member

I don't have any strong data, but my gut is I don't like anything 5ms or more on main thread if we can help it.

@nazarhussain

nazarhussain commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Same check on Platåberget (glamsterdam-devnet-8, 200M gas limit): 1000 envelopes with ≥150M gas used (slots 333469 to 346206), all hashes match.

At 200M gas the block access list (BAL) dominates, while txs stay small:

Per block mean p50 p99 max
Gas used 173M 161M 200M 200M
Txs 84 100 168 190
Tx data 46 KiB 41 KiB 111 KiB 177 KiB
BAL 903 KiB 1266 KiB 1604 KiB 1630 KiB
ms per block mean p50 p99 max
Block hash, native keccak (this PR) 1.66 1.95 2.71 5.40
Block hash, ethereum-cryptography keccak 38.63 50.32 66.46 266.44
BAL keccak only, native 1.28 1.76 2.26 5.23
BAL keccak only, ethereum-cryptography 35.82 49.33 63.10 149.66
Transactions trie only 0.31 0.33 0.66 7.26

Largest block on the chain (slot 77962, 2394 txs, 542 KiB): 9.0 ms native, 42.7 ms with ethereum-cryptography.

Apple M3 Pro, Node v24.22.1 (OpenSSL 3.5.9).

@nazarhussain

Copy link
Copy Markdown
Contributor Author

🤖 Check order: block hash vs signature, following feedback on the spec PR.

The signature check is not cheap: before the ~1 ms BLS verify it computes the envelope signing root, a hash_tree_root over every transaction. Worst case is junk txs up to MAX_TRANSACTIONS_PER_PAYLOAD (1M x 5 B fits in ~9 MiB):

Payload Block hash Signing root BLS verify
2000 txs x 2 KB (3.8 MiB) 15 ms 201 ms 1.1 ms
10k txs x 5 B (0.09 MiB) 20 ms 51 ms 1.1 ms
100k txs x 5 B (0.86 MiB) 174 ms 453 ms 1.1 ms
1M txs x 5 B (9 MiB) 2.6 s 5.3 s 1.1 ms
  • Hash first rejects junk with a wrong hash for the block hash cost alone. A junk envelope crafted with a correct hash pays both checks, ~1.5x the signature-first cost.
  • So spec order is the better trade. Switching this PR to check the hash before the signature.
  • Cost scales with tx count, not bytes, for both checks. The signing root exposure predates this PR (beacon block signing roots include payload txs since bellatrix) and deserves a separate issue.

Apple M3 Pro, Node v24.22.1, single thread.

Follow the spec order. The signature check computes the envelope signing
root over every transaction, which costs more than the block hash, so
rejecting a wrong hash first is the cheaper path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
nazarhussain and others added 5 commits October 28, 2026 16:36
An empty transaction is a deletion in the transactions trie, so trailing
empty transactions do not change the block hash. A builder could sign
such a variant, pass the block hash check and take the seen slot, while
the EL rejects the payload. Reject it before hashing, as
verify_and_notify_new_payload does.

Add a gossip handler test that a signed variant is rejected and not
marked seen, and the committed envelope is still accepted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Plot p50, p90, p99 and mean of
lodestar_gossip_execution_payload_envelope_block_hash_seconds in the
Gossip Execution Payload Envelope Validation row of the networking
dashboard.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Follow consensus-specs#5770 (5d3eb185): REJECT an envelope whose
parent beacon block root differs from the block's parent root, whose
prev_randao or gas_limit differ from the bid, or whose parent_hash,
timestamp or withdrawals differ from the block's post-state.

The post-state is now loaded before the block hash so these cheap checks
reject a mismatched payload before it is hashed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment on lines +216 to +219
// An empty transaction is a deletion in the transactions trie, so trailing empty transactions would not change
// the block hash. The EL rejects such a payload, as `verify_and_notify_new_payload` does before hashing.
for (let i = 0; i < payload.transactions.length; i++) {
if (payload.transactions.getSize(i) === 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is that part of the spec? I know that we added something similar for inclusion lists but I am skeptical why we need all of these checks on gossip here, in case of empty transactions, could we reject this already during serialization?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's part of the specs now, added via ethereum/consensus-specs@a640e7e

nazarhussain and others added 2 commits October 29, 2026 22:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
consensus-specs#5770 (5d3eb18) adds an optional payload field to meta.yaml blocks entries. Record that envelope as on_execution_payload_envelope would, so blocks building on the parent's full payload can be imported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nazarhussain
nazarhussain marked this pull request as ready for review October 30, 2026 11:47
@nazarhussain
nazarhussain requested a review from a team as a code owner October 30, 2026 11:47
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T11:49:13.845910Z 1b66137 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants