|
8 | 8 | ) |
9 | 9 | from eth_consensus_specs.test.helpers.gloas.bid import ( |
10 | 10 | activate_builders, |
| 11 | + append_head_with_requests, |
11 | 12 | build_signed_bid, |
12 | 13 | get_blocks_meta, |
13 | 14 | record_block_in_store, |
14 | 15 | record_head_payload, |
15 | 16 | setup_store_advanced_for_bid, |
| 17 | + setup_store_finalized_with_head_payment, |
16 | 18 | setup_store_finalized_with_pending_payment, |
17 | 19 | ) |
18 | 20 | from eth_consensus_specs.test.helpers.gloas.proposer_preferences import ( |
@@ -1323,6 +1325,317 @@ def test_gossip_execution_payload_bid__reject_builder_not_payload_version(spec, |
1323 | 1325 | yield "messages", "meta", messages |
1324 | 1326 |
|
1325 | 1327 |
|
| 1328 | +@with_gloas_and_later |
| 1329 | +@spec_state_test_with_matching_config |
| 1330 | +def test_gossip_execution_payload_bid__ignore_builder_exit_in_parent_payload(spec, state): |
| 1331 | + """A bid from a builder that the full parent's payload exits is ignored. |
| 1332 | +
|
| 1333 | + The parent's execution requests are applied by its descendant block, so the |
| 1334 | + parent block's post-state still shows the builder as active. Block |
| 1335 | + processing applies the exit before validating the bid, so a bid that |
| 1336 | + validation accepts here sinks the proposal that includes it. |
| 1337 | + """ |
| 1338 | + anchor_state = state.copy() |
| 1339 | + yield "topic", "meta", "execution_payload_bid" |
| 1340 | + |
| 1341 | + builder_index = spec.BuilderIndex(0) |
| 1342 | + store, blocks, _ = setup_store_advanced_for_bid(spec, state) |
| 1343 | + builder = state.builders[builder_index] |
| 1344 | + requests = spec.ExecutionRequests( |
| 1345 | + builder_exits=spec.BuilderExitRequests.of( |
| 1346 | + spec.BuilderExitRequest( |
| 1347 | + source_address=builder.execution_address, |
| 1348 | + pubkey=builder.pubkey, |
| 1349 | + ) |
| 1350 | + ), |
| 1351 | + ) |
| 1352 | + parent_root = append_head_with_requests(spec, state, store, blocks, requests) |
| 1353 | + finalized_checkpoint_meta = activate_builders(spec, state, store, blocks) |
| 1354 | + assert spec.is_active_builder(state, builder_index) |
| 1355 | + head_payload = record_head_payload(spec, state, store, blocks, execution_requests=requests) |
| 1356 | + yield "state", anchor_state |
| 1357 | + for signed in blocks: |
| 1358 | + yield get_filename(signed), signed |
| 1359 | + yield "blocks", "meta", get_blocks_meta(blocks, head_payload) |
| 1360 | + yield "finalized_checkpoint", "meta", finalized_checkpoint_meta |
| 1361 | + |
| 1362 | + time_ms = spec.compute_time_at_slot_ms(store, state.slot) |
| 1363 | + yield "current_time_ms", "meta", int(time_ms) |
| 1364 | + messages = [] |
| 1365 | + seen, common_fee, parent_gas_limit, proposal_slot, parent_block_hash, time_ms = yield from ( |
| 1366 | + _seed_bid_context(spec, state, store, head_payload, messages, time_ms) |
| 1367 | + ) |
| 1368 | + |
| 1369 | + signed_bid = build_signed_bid( |
| 1370 | + spec, |
| 1371 | + state, |
| 1372 | + builder_index=builder_index, |
| 1373 | + slot=proposal_slot, |
| 1374 | + parent_block_hash=parent_block_hash, |
| 1375 | + parent_block_root=parent_root, |
| 1376 | + fee_recipient=common_fee, |
| 1377 | + gas_limit=parent_gas_limit, |
| 1378 | + value=spec.Gwei(1), |
| 1379 | + ) |
| 1380 | + yield get_filename(signed_bid), signed_bid |
| 1381 | + |
| 1382 | + time_ms += 40 |
| 1383 | + result, reason = run_validate_gossip( |
| 1384 | + spec, |
| 1385 | + seen=seen, |
| 1386 | + store=store, |
| 1387 | + signed_execution_payload_bid=signed_bid, |
| 1388 | + current_time_ms=time_ms, |
| 1389 | + ) |
| 1390 | + assert result == "ignore" |
| 1391 | + assert reason == "builder may exit" |
| 1392 | + messages.append( |
| 1393 | + { |
| 1394 | + "current_time_ms": int(time_ms), |
| 1395 | + "message": get_filename(signed_bid), |
| 1396 | + "expected": result, |
| 1397 | + "reason": reason, |
| 1398 | + } |
| 1399 | + ) |
| 1400 | + |
| 1401 | + yield "messages", "meta", messages |
| 1402 | + |
| 1403 | + |
| 1404 | +@with_gloas_and_later |
| 1405 | +@spec_state_test_with_matching_config |
| 1406 | +def test_gossip_execution_payload_bid__valid_parent_exit_unknown_pubkey(spec, state): |
| 1407 | + """A bid is valid when the full parent's exit request names no known builder. |
| 1408 | +
|
| 1409 | + The request's pubkey does not match the bid's builder, so the exit check |
| 1410 | + does not flag the bid. |
| 1411 | + """ |
| 1412 | + anchor_state = state.copy() |
| 1413 | + yield "topic", "meta", "execution_payload_bid" |
| 1414 | + |
| 1415 | + builder_index = spec.BuilderIndex(0) |
| 1416 | + store, blocks, _ = setup_store_advanced_for_bid(spec, state) |
| 1417 | + builder = state.builders[builder_index] |
| 1418 | + unknown_pubkey = spec.BLSPubkey(b"\xab" * 48) |
| 1419 | + assert unknown_pubkey not in [b.pubkey for b in state.builders] |
| 1420 | + requests = spec.ExecutionRequests( |
| 1421 | + builder_exits=spec.BuilderExitRequests.of( |
| 1422 | + spec.BuilderExitRequest( |
| 1423 | + source_address=builder.execution_address, |
| 1424 | + pubkey=unknown_pubkey, |
| 1425 | + ) |
| 1426 | + ), |
| 1427 | + ) |
| 1428 | + parent_root = append_head_with_requests(spec, state, store, blocks, requests) |
| 1429 | + finalized_checkpoint_meta = activate_builders(spec, state, store, blocks) |
| 1430 | + assert spec.is_active_builder(state, builder_index) |
| 1431 | + head_payload = record_head_payload(spec, state, store, blocks, execution_requests=requests) |
| 1432 | + yield "state", anchor_state |
| 1433 | + for signed in blocks: |
| 1434 | + yield get_filename(signed), signed |
| 1435 | + yield "blocks", "meta", get_blocks_meta(blocks, head_payload) |
| 1436 | + yield "finalized_checkpoint", "meta", finalized_checkpoint_meta |
| 1437 | + |
| 1438 | + time_ms = spec.compute_time_at_slot_ms(store, state.slot) |
| 1439 | + yield "current_time_ms", "meta", int(time_ms) |
| 1440 | + messages = [] |
| 1441 | + seen, common_fee, parent_gas_limit, proposal_slot, parent_block_hash, time_ms = yield from ( |
| 1442 | + _seed_bid_context(spec, state, store, head_payload, messages, time_ms) |
| 1443 | + ) |
| 1444 | + |
| 1445 | + signed_bid = build_signed_bid( |
| 1446 | + spec, |
| 1447 | + state, |
| 1448 | + builder_index=builder_index, |
| 1449 | + slot=proposal_slot, |
| 1450 | + parent_block_hash=parent_block_hash, |
| 1451 | + parent_block_root=parent_root, |
| 1452 | + fee_recipient=common_fee, |
| 1453 | + gas_limit=parent_gas_limit, |
| 1454 | + value=spec.Gwei(1), |
| 1455 | + ) |
| 1456 | + yield get_filename(signed_bid), signed_bid |
| 1457 | + |
| 1458 | + time_ms += 40 |
| 1459 | + result, reason = run_validate_gossip( |
| 1460 | + spec, |
| 1461 | + seen=seen, |
| 1462 | + store=store, |
| 1463 | + signed_execution_payload_bid=signed_bid, |
| 1464 | + current_time_ms=time_ms, |
| 1465 | + ) |
| 1466 | + assert result == "valid" |
| 1467 | + assert reason is None |
| 1468 | + messages.append( |
| 1469 | + { |
| 1470 | + "current_time_ms": int(time_ms), |
| 1471 | + "message": get_filename(signed_bid), |
| 1472 | + "expected": result, |
| 1473 | + } |
| 1474 | + ) |
| 1475 | + |
| 1476 | + yield "messages", "meta", messages |
| 1477 | + |
| 1478 | + |
| 1479 | +@with_gloas_and_later |
| 1480 | +@spec_state_test_with_matching_config |
| 1481 | +def test_gossip_execution_payload_bid__valid_parent_exit_wrong_source_address(spec, state): |
| 1482 | + """A bid is valid when the full parent's exit request is not authorized. |
| 1483 | +
|
| 1484 | + The request's source address is not the builder's execution address, so the |
| 1485 | + exit check does not flag the bid. |
| 1486 | + """ |
| 1487 | + anchor_state = state.copy() |
| 1488 | + yield "topic", "meta", "execution_payload_bid" |
| 1489 | + |
| 1490 | + builder_index = spec.BuilderIndex(0) |
| 1491 | + store, blocks, _ = setup_store_advanced_for_bid(spec, state) |
| 1492 | + builder = state.builders[builder_index] |
| 1493 | + wrong_address = spec.ExecutionAddress(b"\xff" * 20) |
| 1494 | + assert builder.execution_address != wrong_address |
| 1495 | + requests = spec.ExecutionRequests( |
| 1496 | + builder_exits=spec.BuilderExitRequests.of( |
| 1497 | + spec.BuilderExitRequest( |
| 1498 | + source_address=wrong_address, |
| 1499 | + pubkey=builder.pubkey, |
| 1500 | + ) |
| 1501 | + ), |
| 1502 | + ) |
| 1503 | + parent_root = append_head_with_requests(spec, state, store, blocks, requests) |
| 1504 | + finalized_checkpoint_meta = activate_builders(spec, state, store, blocks) |
| 1505 | + assert spec.is_active_builder(state, builder_index) |
| 1506 | + head_payload = record_head_payload(spec, state, store, blocks, execution_requests=requests) |
| 1507 | + yield "state", anchor_state |
| 1508 | + for signed in blocks: |
| 1509 | + yield get_filename(signed), signed |
| 1510 | + yield "blocks", "meta", get_blocks_meta(blocks, head_payload) |
| 1511 | + yield "finalized_checkpoint", "meta", finalized_checkpoint_meta |
| 1512 | + |
| 1513 | + time_ms = spec.compute_time_at_slot_ms(store, state.slot) |
| 1514 | + yield "current_time_ms", "meta", int(time_ms) |
| 1515 | + messages = [] |
| 1516 | + seen, common_fee, parent_gas_limit, proposal_slot, parent_block_hash, time_ms = yield from ( |
| 1517 | + _seed_bid_context(spec, state, store, head_payload, messages, time_ms) |
| 1518 | + ) |
| 1519 | + |
| 1520 | + signed_bid = build_signed_bid( |
| 1521 | + spec, |
| 1522 | + state, |
| 1523 | + builder_index=builder_index, |
| 1524 | + slot=proposal_slot, |
| 1525 | + parent_block_hash=parent_block_hash, |
| 1526 | + parent_block_root=parent_root, |
| 1527 | + fee_recipient=common_fee, |
| 1528 | + gas_limit=parent_gas_limit, |
| 1529 | + value=spec.Gwei(1), |
| 1530 | + ) |
| 1531 | + yield get_filename(signed_bid), signed_bid |
| 1532 | + |
| 1533 | + time_ms += 40 |
| 1534 | + result, reason = run_validate_gossip( |
| 1535 | + spec, |
| 1536 | + seen=seen, |
| 1537 | + store=store, |
| 1538 | + signed_execution_payload_bid=signed_bid, |
| 1539 | + current_time_ms=time_ms, |
| 1540 | + ) |
| 1541 | + assert result == "valid" |
| 1542 | + assert reason is None |
| 1543 | + messages.append( |
| 1544 | + { |
| 1545 | + "current_time_ms": int(time_ms), |
| 1546 | + "message": get_filename(signed_bid), |
| 1547 | + "expected": result, |
| 1548 | + } |
| 1549 | + ) |
| 1550 | + |
| 1551 | + yield "messages", "meta", messages |
| 1552 | + |
| 1553 | + |
| 1554 | +@with_gloas_and_later |
| 1555 | +@spec_state_test_with_matching_config |
| 1556 | +def test_gossip_execution_payload_bid__ignore_builder_exit_with_pending_balance(spec, state): |
| 1557 | + """A bid is ignored when a pending balance would block the requested exit. |
| 1558 | +
|
| 1559 | + The full parent's bid pays builder 0, and only a descendant block settles a |
| 1560 | + payment, so the builder has a pending balance when the parent's authorized |
| 1561 | + exit request is applied. That blocks the exit, so block processing keeps the |
| 1562 | + builder active and accepts the bid. Validation does not model the pending |
| 1563 | + balance, so it ignores the bid regardless. |
| 1564 | + """ |
| 1565 | + anchor_state = state.copy() |
| 1566 | + yield "topic", "meta", "execution_payload_bid" |
| 1567 | + |
| 1568 | + builder_index = spec.BuilderIndex(0) |
| 1569 | + builder = state.builders[builder_index] |
| 1570 | + requests = spec.ExecutionRequests( |
| 1571 | + builder_exits=spec.BuilderExitRequests.of( |
| 1572 | + spec.BuilderExitRequest( |
| 1573 | + source_address=builder.execution_address, |
| 1574 | + pubkey=builder.pubkey, |
| 1575 | + ) |
| 1576 | + ), |
| 1577 | + ) |
| 1578 | + store, blocks, parent_root, builder_index, pending_value = ( |
| 1579 | + setup_store_finalized_with_head_payment(spec, state, requests) |
| 1580 | + ) |
| 1581 | + assert pending_value > 0 |
| 1582 | + assert spec.is_active_builder(state, builder_index) |
| 1583 | + head_payload = record_head_payload(spec, state, store, blocks, execution_requests=requests) |
| 1584 | + yield "state", anchor_state |
| 1585 | + for signed in blocks: |
| 1586 | + yield get_filename(signed), signed |
| 1587 | + yield "blocks", "meta", get_blocks_meta(blocks, head_payload) |
| 1588 | + |
| 1589 | + time_ms = spec.compute_time_at_slot_ms(store, state.slot) |
| 1590 | + yield "current_time_ms", "meta", int(time_ms) |
| 1591 | + messages = [] |
| 1592 | + seen, common_fee, parent_gas_limit, proposal_slot, parent_block_hash, time_ms = yield from ( |
| 1593 | + _seed_bid_context(spec, state, store, head_payload, messages, time_ms) |
| 1594 | + ) |
| 1595 | + # The pending payment survives the advance to the bid's slot, which is what |
| 1596 | + # blocks the exit that validation assumes. |
| 1597 | + advanced_state = store.block_states[parent_root].copy() |
| 1598 | + spec.process_slots(advanced_state, proposal_slot) |
| 1599 | + assert ( |
| 1600 | + spec.get_pending_balance_to_withdraw_for_builder(advanced_state, builder_index) |
| 1601 | + == pending_value |
| 1602 | + ) |
| 1603 | + |
| 1604 | + signed_bid = build_signed_bid( |
| 1605 | + spec, |
| 1606 | + state, |
| 1607 | + builder_index=builder_index, |
| 1608 | + slot=proposal_slot, |
| 1609 | + parent_block_hash=parent_block_hash, |
| 1610 | + parent_block_root=parent_root, |
| 1611 | + fee_recipient=common_fee, |
| 1612 | + gas_limit=parent_gas_limit, |
| 1613 | + value=spec.Gwei(1), |
| 1614 | + ) |
| 1615 | + yield get_filename(signed_bid), signed_bid |
| 1616 | + |
| 1617 | + time_ms += 40 |
| 1618 | + result, reason = run_validate_gossip( |
| 1619 | + spec, |
| 1620 | + seen=seen, |
| 1621 | + store=store, |
| 1622 | + signed_execution_payload_bid=signed_bid, |
| 1623 | + current_time_ms=time_ms, |
| 1624 | + ) |
| 1625 | + assert result == "ignore" |
| 1626 | + assert reason == "builder may exit" |
| 1627 | + messages.append( |
| 1628 | + { |
| 1629 | + "current_time_ms": int(time_ms), |
| 1630 | + "message": get_filename(signed_bid), |
| 1631 | + "expected": result, |
| 1632 | + "reason": reason, |
| 1633 | + } |
| 1634 | + ) |
| 1635 | + |
| 1636 | + yield "messages", "meta", messages |
| 1637 | + |
| 1638 | + |
1326 | 1639 | @with_gloas_and_later |
1327 | 1640 | @spec_state_test_with_matching_config |
1328 | 1641 | def test_gossip_execution_payload_bid__reject_too_many_blobs(spec, state): |
|
0 commit comments