From 9f169a81b2915fa2c888835a8ee8ad821ed2fe00 Mon Sep 17 00:00:00 2001 From: Stefan Bratanov Date: Thu, 21 Sep 2023 10:40:12 +0800 Subject: [PATCH] Apply proposer boost to first block in case of equivocation (#7528) --- .../forkchoice/ForkChoiceTestExecutor.java | 2 + .../teku/ethtests/finder/TestDefinition.java | 46 +++---- .../networks/Eth2NetworkConfiguration.java | 17 +++ .../generator/AttesterSlashingGenerator.java | 5 +- .../teku/spec/generator/ChainBuilder.java | 2 +- .../forkchoice/ForkChoice.java | 48 +++++--- .../forkchoice/ForkChoiceTest.java | 114 ++++++++++-------- .../beaconchain/BeaconChainController.java | 1 + .../teku/cli/options/Eth2NetworkOptions.java | 13 ++ 9 files changed, 151 insertions(+), 97 deletions(-) diff --git a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/forkchoice/ForkChoiceTestExecutor.java b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/forkchoice/ForkChoiceTestExecutor.java index a698812f127..a5eaa610384 100644 --- a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/forkchoice/ForkChoiceTestExecutor.java +++ b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/forkchoice/ForkChoiceTestExecutor.java @@ -17,6 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static tech.pegasys.teku.infrastructure.async.SafeFutureAssert.safeJoin; import static tech.pegasys.teku.infrastructure.time.TimeUtilities.secondsToMillis; +import static tech.pegasys.teku.networks.Eth2NetworkConfiguration.DEFAULT_FORK_CHOICE_PROPOSER_BOOST_UNIQUENESS_ENABLED; import static tech.pegasys.teku.networks.Eth2NetworkConfiguration.DEFAULT_FORK_CHOICE_UPDATE_HEAD_ON_BLOCK_IMPORT_ENABLED; import com.google.common.collect.ImmutableMap; @@ -128,6 +129,7 @@ spec, new SignedBlockAndState(anchorBlock, anchorState)), new TickProcessor(spec, recentChainData), transitionBlockValidator, DEFAULT_FORK_CHOICE_UPDATE_HEAD_ON_BLOCK_IMPORT_ENABLED, + DEFAULT_FORK_CHOICE_PROPOSER_BOOST_UNIQUENESS_ENABLED, storageSystem.getMetricsSystem()); final ExecutionLayerChannelStub executionLayer = new ExecutionLayerChannelStub(spec, false, Optional.empty()); diff --git a/eth-tests/src/main/java/tech/pegasys/teku/ethtests/finder/TestDefinition.java b/eth-tests/src/main/java/tech/pegasys/teku/ethtests/finder/TestDefinition.java index 31748bf7288..72a8166d77e 100644 --- a/eth-tests/src/main/java/tech/pegasys/teku/ethtests/finder/TestDefinition.java +++ b/eth-tests/src/main/java/tech/pegasys/teku/ethtests/finder/TestDefinition.java @@ -60,37 +60,21 @@ public Spec getSpec() { } private void createSpec() { - final Eth2Network network; - final SpecMilestone highestSupportedMilestone; - switch (configName) { - case TestSpecConfig.MAINNET: - network = Eth2Network.MAINNET; - break; - case TestSpecConfig.MINIMAL: - network = Eth2Network.MINIMAL; - break; - default: - throw new IllegalArgumentException("Unknown configName: " + configName); - } - switch (fork) { - case TestFork.PHASE0: - highestSupportedMilestone = SpecMilestone.PHASE0; - break; - case TestFork.ALTAIR: - highestSupportedMilestone = SpecMilestone.ALTAIR; - break; - case TestFork.BELLATRIX: - highestSupportedMilestone = SpecMilestone.BELLATRIX; - break; - case TestFork.CAPELLA: - highestSupportedMilestone = SpecMilestone.CAPELLA; - break; - case TestFork.DENEB: - highestSupportedMilestone = SpecMilestone.DENEB; - break; - default: - throw new IllegalArgumentException("Unknown fork: " + fork); - } + final Eth2Network network = + switch (configName) { + case TestSpecConfig.MAINNET -> Eth2Network.MAINNET; + case TestSpecConfig.MINIMAL -> Eth2Network.MINIMAL; + default -> throw new IllegalArgumentException("Unknown configName: " + configName); + }; + final SpecMilestone highestSupportedMilestone = + switch (fork) { + case TestFork.PHASE0 -> SpecMilestone.PHASE0; + case TestFork.ALTAIR -> SpecMilestone.ALTAIR; + case TestFork.BELLATRIX -> SpecMilestone.BELLATRIX; + case TestFork.CAPELLA -> SpecMilestone.CAPELLA; + case TestFork.DENEB -> SpecMilestone.DENEB; + default -> throw new IllegalArgumentException("Unknown fork: " + fork); + }; spec = TestSpecFactory.create(highestSupportedMilestone, network); } diff --git a/ethereum/networks/src/main/java/tech/pegasys/teku/networks/Eth2NetworkConfiguration.java b/ethereum/networks/src/main/java/tech/pegasys/teku/networks/Eth2NetworkConfiguration.java index 5bc7f65ac62..e95adb40fe6 100644 --- a/ethereum/networks/src/main/java/tech/pegasys/teku/networks/Eth2NetworkConfiguration.java +++ b/ethereum/networks/src/main/java/tech/pegasys/teku/networks/Eth2NetworkConfiguration.java @@ -48,6 +48,7 @@ public class Eth2NetworkConfiguration { private static final int DEFAULT_STARTUP_TIMEOUT_SECONDS = 30; public static final boolean DEFAULT_FORK_CHOICE_UPDATE_HEAD_ON_BLOCK_IMPORT_ENABLED = false; + public static final boolean DEFAULT_FORK_CHOICE_PROPOSER_BOOST_UNIQUENESS_ENABLED = false; public static final String INITIAL_STATE_URL_PATH = "eth/v2/debug/beacon/states/finalized"; // 26 thousand years should be enough @@ -70,6 +71,7 @@ public class Eth2NetworkConfiguration { private final Optional trustedSetup; private final boolean forkChoiceUpdateHeadOnBlockImportEnabled; + private final boolean forkChoiceProposerBoostUniquenessEnabled; private final Optional terminalBlockHashOverride; private final Optional totalTerminalDifficultyOverride; private final Optional terminalBlockHashEpochOverride; @@ -89,6 +91,7 @@ private Eth2NetworkConfiguration( final Optional eth1DepositContractDeployBlock, final Optional trustedSetup, final boolean forkChoiceUpdateHeadOnBlockImportEnabled, + final boolean forkChoiceProposerBoostUniquenessEnabled, final Optional altairForkEpoch, final Optional bellatrixForkEpoch, final Optional capellaForkEpoch, @@ -117,6 +120,7 @@ private Eth2NetworkConfiguration( this.eth1DepositContractDeployBlock = eth1DepositContractDeployBlock; this.trustedSetup = trustedSetup; this.forkChoiceUpdateHeadOnBlockImportEnabled = forkChoiceUpdateHeadOnBlockImportEnabled; + this.forkChoiceProposerBoostUniquenessEnabled = forkChoiceProposerBoostUniquenessEnabled; this.terminalBlockHashOverride = terminalBlockHashOverride; this.totalTerminalDifficultyOverride = totalTerminalDifficultyOverride; this.terminalBlockHashEpochOverride = terminalBlockHashEpochOverride; @@ -189,6 +193,10 @@ public boolean isForkChoiceUpdateHeadOnBlockImportEnabled() { return forkChoiceUpdateHeadOnBlockImportEnabled; } + public boolean isForkChoiceProposerBoostUniquenessEnabled() { + return forkChoiceProposerBoostUniquenessEnabled; + } + public Optional getForkEpoch(final SpecMilestone specMilestone) { return switch (specMilestone) { case ALTAIR -> altairForkEpoch; @@ -248,6 +256,8 @@ public static class Builder { private Spec spec; private boolean forkChoiceUpdateHeadOnBlockImportEnabled = DEFAULT_FORK_CHOICE_UPDATE_HEAD_ON_BLOCK_IMPORT_ENABLED; + private boolean forkChoiceProposerBoostUniquenessEnabled = + DEFAULT_FORK_CHOICE_PROPOSER_BOOST_UNIQUENESS_ENABLED; public void spec(Spec spec) { this.spec = spec; @@ -310,6 +320,7 @@ public Eth2NetworkConfiguration build() { eth1DepositContractDeployBlock, trustedSetup, forkChoiceUpdateHeadOnBlockImportEnabled, + forkChoiceProposerBoostUniquenessEnabled, altairForkEpoch, bellatrixForkEpoch, capellaForkEpoch, @@ -415,6 +426,12 @@ public Builder forkChoiceUpdateHeadOnBlockImportEnabled( return this; } + public Builder forkChoiceProposerBoostUniquenessEnabled( + final boolean forkChoiceProposerBoostUniquenessEnabled) { + this.forkChoiceProposerBoostUniquenessEnabled = forkChoiceProposerBoostUniquenessEnabled; + return this; + } + public Builder altairForkEpoch(final UInt64 altairForkEpoch) { this.altairForkEpoch = Optional.of(altairForkEpoch); return this; diff --git a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/generator/AttesterSlashingGenerator.java b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/generator/AttesterSlashingGenerator.java index cf5fbf2af32..cd00de4af58 100644 --- a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/generator/AttesterSlashingGenerator.java +++ b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/generator/AttesterSlashingGenerator.java @@ -50,7 +50,10 @@ public AttesterSlashingGenerator(final Spec spec, final List validat public AttesterSlashing createAttesterSlashingForAttestation( final Attestation goodAttestation, final SignedBlockAndState blockAndState) { if (!goodAttestation.getData().getSlot().equals(blockAndState.getSlot())) { - throw new RuntimeException("Good attestation slot and input block slot should match"); + throw new RuntimeException( + String.format( + "Good attestation slot %s and input block slot %s should match", + goodAttestation.getData().getSlot(), blockAndState.getSlot())); } AttestationUtil attestationUtil = spec.atSlot(blockAndState.getSlot()).getAttestationUtil(); IndexedAttestation indexedGoodAttestation = diff --git a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/generator/ChainBuilder.java b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/generator/ChainBuilder.java index 8587c17e7f7..bea535f99e7 100644 --- a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/generator/ChainBuilder.java +++ b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/generator/ChainBuilder.java @@ -353,7 +353,7 @@ public List generateBlocksUpToSlot(final UInt64 slot) { final List generated = new ArrayList<>(); SignedBlockAndState latestBlock = getLatestBlockAndState(); - while (latestBlock.getState().getSlot().compareTo(slot) < 0) { + while (latestBlock.getState().getSlot().isLessThan(slot)) { latestBlock = generateNextBlock(); generated.add(latestBlock); } diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoice.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoice.java index ef045302bff..a6c9ea01162 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoice.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoice.java @@ -93,6 +93,7 @@ public class ForkChoice implements ForkChoiceUpdatedResultSubscriber { private final ForkChoiceNotifier forkChoiceNotifier; private final MergeTransitionBlockValidator transitionBlockValidator; private final boolean forkChoiceUpdateHeadOnBlockImportEnabled; + private final boolean forkChoiceProposerBoostUniquenessEnabled; private final AttestationStateSelector attestationStateSelector; private final DeferredAttestations deferredAttestations = new DeferredAttestations(); @@ -111,6 +112,7 @@ public ForkChoice( final TickProcessor tickProcessor, final MergeTransitionBlockValidator transitionBlockValidator, final boolean forkChoiceUpdateHeadOnBlockImportEnabled, + final boolean forkChoiceProposerBoostUniquenessEnabled, final MetricsSystem metricsSystem) { this.spec = spec; this.forkChoiceExecutor = forkChoiceExecutor; @@ -123,6 +125,7 @@ public ForkChoice( new AttestationStateSelector(spec, recentChainData, metricsSystem); this.tickProcessor = tickProcessor; this.forkChoiceUpdateHeadOnBlockImportEnabled = forkChoiceUpdateHeadOnBlockImportEnabled; + this.forkChoiceProposerBoostUniquenessEnabled = forkChoiceProposerBoostUniquenessEnabled; recentChainData.subscribeStoreInitialized(this::initializeProtoArrayForkChoice); forkChoiceNotifier.subscribeToForkChoiceUpdatedResult(this); } @@ -150,6 +153,7 @@ public ForkChoice( new TickProcessor(spec, recentChainData), transitionBlockValidator, true, + true, metricsSystem); } @@ -488,23 +492,20 @@ private BlockImportResult importBlockAndState( } switch (blobSidecarsAndValidationResult.getValidationResult()) { - case VALID: - case NOT_REQUIRED: - LOG.debug( - "blobSidecars validation result: {}", blobSidecarsAndValidationResult::toLogString); - - break; - case NOT_AVAILABLE: + case VALID, NOT_REQUIRED -> LOG.debug( + "blobSidecars validation result: {}", blobSidecarsAndValidationResult::toLogString); + case NOT_AVAILABLE -> { LOG.warn( "blobSidecars validation result: {}", blobSidecarsAndValidationResult::toLogString); return BlockImportResult.failedDataAvailabilityCheckNotAvailable( blobSidecarsAndValidationResult.getCause()); - - case INVALID: + } + case INVALID -> { LOG.error( "blobSidecars validation result: {}", blobSidecarsAndValidationResult::toLogString); return BlockImportResult.failedDataAvailabilityCheckInvalid( blobSidecarsAndValidationResult.getCause()); + } } final ForkChoiceStrategy forkChoiceStrategy = getForkChoiceStrategy(); @@ -542,13 +543,8 @@ private BlockImportResult importBlockAndState( blobSidecars, earliestBlobSidecarsSlot); - if (spec.getCurrentSlot(transaction).equals(block.getSlot())) { - final UInt64 millisPerSlot = spec.getMillisPerSlot(block.getSlot()); - final UInt64 timeIntoSlotMillis = getMillisIntoSlot(transaction, millisPerSlot); - - if (isBeforeAttestingInterval(millisPerSlot, timeIntoSlotMillis)) { - transaction.setProposerBoostRoot(block.getRoot()); - } + if (shouldApplyProposerBoost(block, transaction)) { + transaction.setProposerBoostRoot(block.getRoot()); } blockImportPerformance.ifPresent(BlockImportPerformance::transactionReady); @@ -582,6 +578,26 @@ private BlockImportResult importBlockAndState( return result; } + // from consensus-specs/fork-choice: + private boolean shouldApplyProposerBoost( + final SignedBeaconBlock block, final StoreTransaction transaction) { + // get_current_slot(store) == block.slot + if (!spec.getCurrentSlot(transaction).equals(block.getSlot())) { + return false; + } + // is_before_attesting_interval + final UInt64 millisPerSlot = spec.getMillisPerSlot(block.getSlot()); + final UInt64 timeIntoSlotMillis = getMillisIntoSlot(transaction, millisPerSlot); + if (!isBeforeAttestingInterval(millisPerSlot, timeIntoSlotMillis)) { + return false; + } + // is_first_block + if (forkChoiceProposerBoostUniquenessEnabled) { + return transaction.getProposerBoostRoot().isEmpty(); + } + return true; + } + /** * In order to keep track of DataAvailability Window, we need to compute the earliest slot we can * consider data available for the given block. It needs to take in account possible empty slots diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoiceTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoiceTest.java index 09e4b36d1bd..52ff4045690 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoiceTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoiceTest.java @@ -29,6 +29,7 @@ import static tech.pegasys.teku.infrastructure.async.SafeFutureAssert.safeJoin; import static tech.pegasys.teku.infrastructure.unsigned.UInt64.ONE; import static tech.pegasys.teku.infrastructure.unsigned.UInt64.ZERO; +import static tech.pegasys.teku.networks.Eth2NetworkConfiguration.DEFAULT_FORK_CHOICE_PROPOSER_BOOST_UNIQUENESS_ENABLED; import static tech.pegasys.teku.networks.Eth2NetworkConfiguration.DEFAULT_FORK_CHOICE_UPDATE_HEAD_ON_BLOCK_IMPORT_ENABLED; import static tech.pegasys.teku.statetransition.forkchoice.ForkChoice.BLOCK_CREATION_TOLERANCE_MS; @@ -154,6 +155,9 @@ private void setupWithSpec(final Spec spec) { new TickProcessor(spec, recentChainData), transitionBlockValidator, DEFAULT_FORK_CHOICE_UPDATE_HEAD_ON_BLOCK_IMPORT_ENABLED, + // will use DEFAULT_FORK_CHOICE_PROPOSER_BOOST_UNIQUENESS_ENABLED in an upcoming PR + // which will update to the new ref tests and set the const to true + true, metricsSystem); // Starting and mocks @@ -303,6 +307,7 @@ void onBlock_shouldReorgWhenProposerWeightingMakesForkBestChain( new TickProcessor(spec, recentChainData), transitionBlockValidator, DEFAULT_FORK_CHOICE_UPDATE_HEAD_ON_BLOCK_IMPORT_ENABLED, + DEFAULT_FORK_CHOICE_PROPOSER_BOOST_UNIQUENESS_ENABLED, metricsSystem); final UInt64 currentSlot = recentChainData.getCurrentSlot().orElseThrow(); @@ -361,50 +366,49 @@ void onBlock_shouldReorgWhenProposerWeightingMakesForkBestChain( @Test void onBlock_shouldUpdateVotesBasedOnAttestationsInBlocks() { final ChainBuilder forkChain = chainBuilder.fork(); - final SignedBlockAndState forkBlock1 = + final SignedBlockAndState forkBlock = forkChain.generateBlockAtSlot( ONE, BlockOptions.create() .setEth1Data(new Eth1Data(Bytes32.ZERO, UInt64.valueOf(6), Bytes32.ZERO))); - final SignedBlockAndState betterBlock1 = chainBuilder.generateBlockAtSlot(1); + // eventually better chain with an empty block + final SignedBlockAndState betterBlock = chainBuilder.generateNextBlock(1); - importBlock(forkBlock1); - // Should automatically follow the fork as its the first child block - assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock1.getRoot()); + importBlock(forkBlock); + // Should automatically follow the fork as it is the only one imported + assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock.getRoot()); // Add an attestation for the fork so that it initially has higher weight // Otherwise ties are split based on the hash which is too hard to control in the test final BlockOptions forkBlockOptions = BlockOptions.create(); forkChain - .streamValidAttestationsWithTargetBlock(forkBlock1) + .streamValidAttestationsWithTargetBlock(forkBlock) .limit(1) .forEach(forkBlockOptions::addAttestation); - final SignedBlockAndState forkBlock2 = - forkChain.generateBlockAtSlot(forkBlock1.getSlot().plus(1), forkBlockOptions); - importBlock(forkBlock2); + final SignedBlockAndState forkBlock1 = forkChain.generateNextBlock(forkBlockOptions); + importBlock(forkBlock1); // The fork is still the only option so gets selected - assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock2.getRoot()); + assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock1.getRoot()); // Now import what will become the canonical chain - importBlock(betterBlock1); + importBlock(betterBlock); // Process head to ensure we clear any additional proposer weighting for this first block. // Should still pick forkBlock as it's the best option even though we have a competing chain processHead(ONE); - assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock2.getRoot()); + assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock1.getRoot()); // Import a block with two attestations which makes this chain better than the fork final BlockOptions options = BlockOptions.create(); chainBuilder - .streamValidAttestationsWithTargetBlock(betterBlock1) + .streamValidAttestationsWithTargetBlock(betterBlock) .limit(2) .forEach(options::addAttestation); - final SignedBlockAndState blockWithAttestations = - chainBuilder.generateBlockAtSlot(UInt64.valueOf(2), options); + final SignedBlockAndState blockWithAttestations = chainBuilder.generateNextBlock(options); importBlock(blockWithAttestations); // Haven't run fork choice so won't have re-orged yet - fork still has more applied votes - assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock2.getRoot()); + assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock1.getRoot()); // When attestations are applied we should switch away from the fork to our better chain processHead(blockWithAttestations.getSlot()); @@ -414,69 +418,64 @@ void onBlock_shouldUpdateVotesBasedOnAttestationsInBlocks() { @Test void onBlock_shouldUpdateVotesBasedOnAttesterSlashingEquivocationsInBlocks() { final ChainBuilder forkChain = chainBuilder.fork(); - final SignedBlockAndState forkBlock1 = - forkChain.generateBlockAtSlot( - ONE, + final SignedBlockAndState forkBlock = + forkChain.generateNextBlock( BlockOptions.create() .setEth1Data(new Eth1Data(Bytes32.ZERO, UInt64.valueOf(6), Bytes32.ZERO))); - final SignedBlockAndState betterBlock1 = chainBuilder.generateBlockAtSlot(1); + // eventually better chain with an empty block + final SignedBlockAndState betterBlock = chainBuilder.generateNextBlock(1); - importBlock(forkBlock1); - // Should automatically follow the fork as its the first child block - assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock1.getRoot()); + importBlock(forkBlock); + + // Should automatically follow the fork as it is the only one imported + assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock.getRoot()); // Add an attestation for the fork so that it initially has higher weight // Otherwise ties are split based on the hash which is too hard to control in the test final BlockOptions forkBlockOptions = BlockOptions.create(); - List forkAttestations = - forkChain - .streamValidAttestationsWithTargetBlock(forkBlock1) - .limit(2) - .collect(Collectors.toList()); + final List forkAttestations = + forkChain.streamValidAttestationsWithTargetBlock(forkBlock).limit(2).toList(); forkAttestations.forEach(forkBlockOptions::addAttestation); - final SignedBlockAndState forkBlock2 = - forkChain.generateBlockAtSlot(forkBlock1.getSlot().plus(1), forkBlockOptions); - importBlock(forkBlock2); + final SignedBlockAndState forkBlock1 = forkChain.generateNextBlock(forkBlockOptions); + importBlock(forkBlock1); // The fork is still the only option so gets selected - assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock2.getRoot()); + assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock1.getRoot()); // Now import what will become the canonical chain - importBlock(betterBlock1); + importBlock(betterBlock); // Process head to ensure we clear any additional proposer weighting for this first block. // Should still pick forkBlock as it's the best option even though we have a competing chain processHead(ONE); - assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock2.getRoot()); + assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock1.getRoot()); // Import a block with one attestation on what will be better chain final BlockOptions options = BlockOptions.create(); chainBuilder - .streamValidAttestationsWithTargetBlock(betterBlock1) + .streamValidAttestationsWithTargetBlock(betterBlock) .limit(1) .forEach(options::addAttestation); - final SignedBlockAndState blockWithAttestations = - chainBuilder.generateBlockAtSlot(UInt64.valueOf(2), options); + final SignedBlockAndState blockWithAttestations = chainBuilder.generateNextBlock(options); importBlock(blockWithAttestations); // Haven't run fork choice so won't have re-orged yet - fork still has more applied votes - assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock2.getRoot()); + assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock1.getRoot()); // Verify that fork is still better processHead(blockWithAttestations.getSlot()); - assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock2.getRoot()); + assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock1.getRoot()); // Add 2 AttesterSlashing on betterBlock chain, so it will become finally better final BlockOptions options2 = BlockOptions.create(); forkAttestations.forEach( attestation -> options2.addAttesterSlashing( - chainBuilder.createAttesterSlashingForAttestation(attestation, forkBlock1))); - final SignedBlockAndState blockWithAttesterSlashings = - chainBuilder.generateBlockAtSlot(UInt64.valueOf(3), options2); + chainBuilder.createAttesterSlashingForAttestation(attestation, forkBlock))); + final SignedBlockAndState blockWithAttesterSlashings = chainBuilder.generateNextBlock(options2); importBlock(blockWithAttesterSlashings); // Haven't run fork choice so won't have re-orged yet - fork still has more applied votes - assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock2.getRoot()); + assertThat(recentChainData.getBestBlockRoot()).contains(forkBlock1.getRoot()); // When attester slashings are applied we should switch away from the fork to our better chain processHead(blockWithAttesterSlashings.getSlot()); @@ -716,6 +715,27 @@ void onBlock_shouldNotifyOptimisticSyncChangeOnlyWhenImportingOnCanonicalHead() verifyNoMoreInteractions(optimisticSyncStateTracker); } + @Test + void onBlock_shouldApplyProposerBoostToFirstBlock() { + final ChainBuilder forkChain = chainBuilder.fork(); + + final SignedBlockAndState block = chainBuilder.generateNextBlock(); + final SignedBlockAndState forkBlock = forkChain.generateNextBlock(); + + final BlockOptions forkBlockOptions = BlockOptions.create(); + final List forkAttestations = + forkChain.streamValidAttestationsWithTargetBlock(forkBlock).limit(2).toList(); + forkAttestations.forEach(forkBlockOptions::addAttestation); + final SignedBlockAndState forkBlock1 = forkChain.generateNextBlock(forkBlockOptions); + + importBlock(block); + importBlock(forkBlock); + importBlock(forkBlock1); + + // proposer boost is given to the first block despite the fork chain having bigger weight + assertThat(recentChainData.getStore().getProposerBoostRoot()).hasValue(block.getRoot()); + } + @Test void applyHead_shouldSendForkChoiceUpdatedNotification() { final SignedBlockAndState blockAndState = storageSystem.chainUpdater().advanceChainUntil(1); @@ -1074,11 +1094,9 @@ private SignedBlockAndState generateMergeBlock() { ZERO); executionLayer.addPowBlock(terminalBlock); executionLayer.addPowBlock(terminalParentBlock); - final SignedBlockAndState epoch4Block = - chainBuilder.generateBlockAtSlot( - storageSystem.chainUpdater().getHeadSlot().plus(1), - BlockOptions.create().setTerminalBlockHash(terminalBlockHash)); - return epoch4Block; + return chainBuilder.generateBlockAtSlot( + storageSystem.chainUpdater().getHeadSlot().plus(1), + BlockOptions.create().setTerminalBlockHash(terminalBlockHash)); } @Test diff --git a/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java b/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java index 1af52c53e51..532d8cb4695 100644 --- a/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java +++ b/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java @@ -678,6 +678,7 @@ protected void initForkChoice() { new TickProcessor(spec, recentChainData), new MergeTransitionBlockValidator(spec, recentChainData, executionLayer), beaconConfig.eth2NetworkConfig().isForkChoiceUpdateHeadOnBlockImportEnabled(), + beaconConfig.eth2NetworkConfig().isForkChoiceProposerBoostUniquenessEnabled(), metricsSystem); forkChoiceTrigger = new ForkChoiceTrigger(forkChoice); } diff --git a/teku/src/main/java/tech/pegasys/teku/cli/options/Eth2NetworkOptions.java b/teku/src/main/java/tech/pegasys/teku/cli/options/Eth2NetworkOptions.java index 8e7a7453ab5..10719798345 100644 --- a/teku/src/main/java/tech/pegasys/teku/cli/options/Eth2NetworkOptions.java +++ b/teku/src/main/java/tech/pegasys/teku/cli/options/Eth2NetworkOptions.java @@ -166,6 +166,18 @@ public class Eth2NetworkOptions { private boolean forkChoiceUpdateHeadOnBlockImportEnabled = Eth2NetworkConfiguration.DEFAULT_FORK_CHOICE_UPDATE_HEAD_ON_BLOCK_IMPORT_ENABLED; + // https://github.com/Consensys/teku/issues/7537 + @Option( + names = {"--Xfork-choice-proposer-boost-uniqueness-enabled"}, + paramLabel = "", + description = "Apply proposer boost to first block in case of equivocation.", + arity = "0..1", + fallbackValue = "true", + showDefaultValue = Visibility.ALWAYS, + hidden = true) + private boolean forkChoiceProposerBoostUniquenessEnabled = + Eth2NetworkConfiguration.DEFAULT_FORK_CHOICE_PROPOSER_BOOST_UNIQUENESS_ENABLED; + @Option( names = {"--Xeth1-deposit-contract-deploy-block-override"}, hidden = true, @@ -246,6 +258,7 @@ private void configureEth2Network(Eth2NetworkConfiguration.Builder builder) { builder .safeSlotsToImportOptimistically(safeSlotsToImportOptimistically) .forkChoiceUpdateHeadOnBlockImportEnabled(forkChoiceUpdateHeadOnBlockImportEnabled) + .forkChoiceProposerBoostUniquenessEnabled(forkChoiceProposerBoostUniquenessEnabled) .epochsStoreBlobs(epochsStoreBlobs); }