Skip to content

Commit adc1dd5

Browse files
authored
Improve fcu attributes calculation stability (Consensys#8009)
1 parent 25b515c commit adc1dd5

File tree

4 files changed

+112
-80
lines changed

4 files changed

+112
-80
lines changed

Diff for: ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoiceNotifierImpl.java

+52-28
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,14 @@
2323
import tech.pegasys.teku.infrastructure.time.TimeProvider;
2424
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
2525
import tech.pegasys.teku.spec.Spec;
26-
import tech.pegasys.teku.spec.config.SpecConfig;
2726
import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadContext;
2827
import tech.pegasys.teku.spec.executionlayer.ExecutionLayerChannel;
2928
import tech.pegasys.teku.spec.executionlayer.ForkChoiceState;
29+
import tech.pegasys.teku.spec.executionlayer.PayloadBuildingAttributes;
3030
import tech.pegasys.teku.statetransition.forkchoice.ForkChoiceUpdatedResultSubscriber.ForkChoiceUpdatedResultNotification;
31-
import tech.pegasys.teku.statetransition.forkchoice.ProposersDataManager.ProposersDataManagerSubscriber;
3231
import tech.pegasys.teku.storage.client.RecentChainData;
3332

34-
public class ForkChoiceNotifierImpl implements ForkChoiceNotifier, ProposersDataManagerSubscriber {
33+
public class ForkChoiceNotifierImpl implements ForkChoiceNotifier {
3534
private static final Logger LOG = LogManager.getLogger();
3635

3736
private final EventThread eventThread;
@@ -64,7 +63,6 @@ public ForkChoiceNotifierImpl(
6463
this.recentChainData = recentChainData;
6564
this.proposersDataManager = proposersDataManager;
6665
this.timeProvider = timeProvider;
67-
proposersDataManager.subscribeToProposersDataChanges(this);
6866
}
6967

7068
@Override
@@ -107,14 +105,6 @@ public boolean validatorIsConnected(UInt64 validatorIndex, UInt64 currentSlot) {
107105
return proposersDataManager.validatorIsConnected(validatorIndex, currentSlot);
108106
}
109107

110-
@Override
111-
public void onPreparedProposersUpdated() {
112-
eventThread.execute(this::internalUpdatePreparableProposers);
113-
}
114-
115-
@Override
116-
public void onValidatorRegistrationsUpdated() {}
117-
118108
private void internalTerminalBlockReached(Bytes32 executionBlockHash) {
119109
eventThread.checkOnEventThread();
120110
LOG.debug("internalTerminalBlockReached executionBlockHash {}", executionBlockHash);
@@ -198,18 +188,6 @@ private SafeFuture<Optional<ExecutionPayloadContext>> internalGetPayloadId(
198188
}
199189
}
200190

201-
private void internalUpdatePreparableProposers() {
202-
eventThread.checkOnEventThread();
203-
204-
LOG.debug("internalUpdatePreparableProposers");
205-
206-
// Default to the genesis slot if we're pre-genesis.
207-
final UInt64 currentSlot = recentChainData.getCurrentSlot().orElse(SpecConfig.GENESIS_SLOT);
208-
209-
// Update payload attributes in case we now need to propose the next block
210-
updatePayloadAttributes(currentSlot.plus(1));
211-
}
212-
213191
private void internalForkChoiceUpdated(
214192
final ForkChoiceState forkChoiceState, final Optional<UInt64> proposingSlot) {
215193
eventThread.checkOnEventThread();
@@ -220,14 +198,60 @@ private void internalForkChoiceUpdated(
220198

221199
LOG.debug("internalForkChoiceUpdated forkChoiceUpdateData {}", forkChoiceUpdateData);
222200

223-
final Optional<UInt64> attributesSlot =
224-
proposingSlot.or(() -> recentChainData.getCurrentSlot().map(UInt64::increment));
225-
226-
attributesSlot.ifPresent(this::updatePayloadAttributes);
201+
calculatePayloadAttributesSlot(forkChoiceState, proposingSlot)
202+
.ifPresent(this::updatePayloadAttributes);
227203

228204
sendForkChoiceUpdated();
229205
}
230206

207+
/**
208+
* Determine for which slot we should calculate payload attributes (block proposal)
209+
*
210+
* <pre>
211+
* this will guarantee that whenever we calculate a payload attributes for a slot, it will remain stable until:
212+
* 1. next slot attestation due is reached (internalAttestationsDue forcing attributes calculation for next slot)
213+
* OR
214+
* 2. we imported the block for current slot and has become the head
215+
* </pre>
216+
*/
217+
private Optional<UInt64> calculatePayloadAttributesSlot(
218+
final ForkChoiceState forkChoiceState, final Optional<UInt64> proposingSlot) {
219+
if (proposingSlot.isPresent()) {
220+
// We are in the context of a block production, so we should use the proposing slot
221+
return proposingSlot;
222+
}
223+
224+
final Optional<UInt64> currentSlot = recentChainData.getCurrentSlot();
225+
if (currentSlot.isEmpty()) {
226+
// We are pre-genesis, so we don't care about proposing slots
227+
return Optional.empty();
228+
}
229+
230+
final Optional<UInt64> maybeCurrentPayloadAttributesSlot =
231+
forkChoiceUpdateData
232+
.getPayloadBuildingAttributes()
233+
.map(PayloadBuildingAttributes::getProposalSlot);
234+
235+
if (maybeCurrentPayloadAttributesSlot.isPresent()
236+
// we are still in the same slot as the last proposing slot
237+
&& currentSlot.get().equals(maybeCurrentPayloadAttributesSlot.get())
238+
// we have not yet imported our own produced block
239+
&& forkChoiceState.getHeadBlockSlot().isLessThan(maybeCurrentPayloadAttributesSlot.get())) {
240+
241+
LOG.debug(
242+
"current payload attributes slot has been chosen for payload attributes calculation: {}",
243+
currentSlot.get());
244+
245+
// in case we propose two blocks in a row and we fail producing the first block,
246+
// we won't keep using the same first slot because internalAttestationsDue will
247+
// update the payload attributes for the second block slot
248+
return currentSlot;
249+
}
250+
251+
// chain advanced since last proposing slot, we should consider attributes for the next slot
252+
return currentSlot.map(UInt64::increment);
253+
}
254+
231255
private void internalAttestationsDue(final UInt64 slot) {
232256
eventThread.checkOnEventThread();
233257

Diff for: ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ProposersDataManager.java

-19
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import tech.pegasys.teku.infrastructure.async.eventthread.EventThread;
3131
import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory;
3232
import tech.pegasys.teku.infrastructure.ssz.SszList;
33-
import tech.pegasys.teku.infrastructure.subscribers.Subscribers;
3433
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
3534
import tech.pegasys.teku.spec.Spec;
3635
import tech.pegasys.teku.spec.datastructures.blocks.SlotAndBlockRoot;
@@ -59,9 +58,6 @@ public class ProposersDataManager implements SlotEventsChannel {
5958
private final Optional<Eth1Address> proposerDefaultFeeRecipient;
6059
private final boolean forkChoiceUpdatedAlwaysSendPayloadAttribute;
6160

62-
private final Subscribers<ProposersDataManagerSubscriber> proposersDataChangesSubscribers =
63-
Subscribers.create(true);
64-
6561
public ProposersDataManager(
6662
final EventThread eventThread,
6763
final Spec spec,
@@ -88,10 +84,6 @@ public ProposersDataManager(
8884
this.forkChoiceUpdatedAlwaysSendPayloadAttribute = forkChoiceUpdatedAlwaysSendPayloadAttribute;
8985
}
9086

91-
public void subscribeToProposersDataChanges(final ProposersDataManagerSubscriber subscriber) {
92-
proposersDataChangesSubscribers.subscribe(subscriber);
93-
}
94-
9587
@Override
9688
public void onSlot(UInt64 slot) {
9789
// do clean up in the middle of the epoch
@@ -125,8 +117,6 @@ public void onSlot(UInt64 slot) {
125117
public void updatePreparedProposers(
126118
final Collection<BeaconPreparableProposer> preparedProposers, final UInt64 currentSlot) {
127119
updatePreparedProposerCache(preparedProposers, currentSlot);
128-
proposersDataChangesSubscribers.deliver(
129-
ProposersDataManagerSubscriber::onPreparedProposersUpdated);
130120
}
131121

132122
public SafeFuture<Void> updateValidatorRegistrations(
@@ -180,9 +170,6 @@ private void updateValidatorRegistrationCache(
180170
LOG.warn(
181171
"validator index not found for public key {}",
182172
signedValidatorRegistration.getMessage().getPublicKey())));
183-
184-
proposersDataChangesSubscribers.deliver(
185-
ProposersDataManagerSubscriber::onValidatorRegistrationsUpdated);
186173
}
187174

188175
public SafeFuture<Optional<PayloadBuildingAttributes>> calculatePayloadBuildingAttributes(
@@ -302,10 +289,4 @@ public Map<UInt64, RegisteredValidatorInfo> getValidatorRegistrationInfo() {
302289
public boolean isProposerDefaultFeeRecipientDefined() {
303290
return proposerDefaultFeeRecipient.isPresent();
304291
}
305-
306-
public interface ProposersDataManagerSubscriber {
307-
void onPreparedProposersUpdated();
308-
309-
void onValidatorRegistrationsUpdated();
310-
}
311292
}

Diff for: ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoiceNotifierTest.java

+59-10
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,50 @@ void onForkChoiceUpdated_shouldSendNotificationWithPayloadBuildingAttributesForN
258258
verify(executionLayerChannel).engineForkChoiceUpdated(forkChoiceState, Optional.empty());
259259
}
260260

261+
@Test
262+
void onForkChoiceUpdated_shouldSendNotificationWithStableSlot() {
263+
final ForkChoiceState forkChoiceState = getCurrentForkChoiceState();
264+
final BeaconState headState = getHeadState();
265+
266+
// setup two block productions in a row
267+
final UInt64 blockSlot = headState.getSlot().plus(1);
268+
final UInt64 nextBlockSlot = headState.getSlot().plus(2);
269+
270+
final List<PayloadBuildingAttributes> payloadBuildingAttributes =
271+
withProposerForTwoSlots(forkChoiceState, headState, blockSlot, nextBlockSlot);
272+
273+
storageSystem.chainUpdater().setCurrentSlot(blockSlot);
274+
275+
notifyForkChoiceUpdated(forkChoiceState, Optional.of(blockSlot));
276+
verify(executionLayerChannel)
277+
.engineForkChoiceUpdated(forkChoiceState, Optional.of(payloadBuildingAttributes.get(0)));
278+
279+
assertThat(recentChainData.getCurrentSlot()).contains(blockSlot);
280+
281+
// fcu call with same state but no proposerSlot should not cause a new call to the EL
282+
notifyForkChoiceUpdatedVerifyNoNotification(forkChoiceState);
283+
verifyNoMoreInteractions(executionLayerChannel);
284+
285+
// when attestation due arrives, the next proposer attributes should be sent
286+
forkChoiceUpdatedResultNotification = null;
287+
notifier.onAttestationsDue(blockSlot);
288+
assertThat(forkChoiceUpdatedResultNotification).isNotNull();
289+
assertThat(forkChoiceUpdatedResultNotification.payloadAttributes())
290+
.contains(payloadBuildingAttributes.get(1));
291+
verify(executionLayerChannel)
292+
.engineForkChoiceUpdated(forkChoiceState, Optional.of(payloadBuildingAttributes.get(1)));
293+
294+
// we are still on blockSlot, with EL already notified to build nextBlockSlot
295+
notifyForkChoiceUpdatedVerifyNoNotification(forkChoiceState);
296+
verifyNoMoreInteractions(executionLayerChannel);
297+
298+
storageSystem.chainUpdater().setCurrentSlot(nextBlockSlot);
299+
300+
// we are asked to build nextBlockSlot, EL is already notified for that
301+
notifyForkChoiceUpdatedVerifyNoNotification(forkChoiceState, Optional.of(nextBlockSlot));
302+
verifyNoMoreInteractions(executionLayerChannel);
303+
}
304+
261305
@Test
262306
void
263307
onForkChoiceUpdated_shouldSendNotificationWithPayloadBuildingAttributesIfConfiguredToAlwaysSendThem() {
@@ -378,11 +422,7 @@ void onForkChoiceUpdated_shouldSendNotificationOfOrderedPayloadBuildingAttribute
378422
verify(executionLayerChannel)
379423
.engineForkChoiceUpdated(forkChoiceState, Optional.of(payloadBuildingAttributes.get(0)));
380424

381-
storageSystem
382-
.chainUpdater()
383-
.setCurrentSlot(headState.getSlot().plus(1)); // set current slot to 2
384-
385-
notifyForkChoiceUpdated(forkChoiceState); // calculate attributes for slot 3
425+
notifier.onAttestationsDue(blockSlot); // calculate attributes for slot 3
386426

387427
// expect attributes for slot 3
388428
verify(executionLayerChannel)
@@ -569,18 +609,16 @@ void onPreparedProposersUpdated_shouldNotIncludePayloadBuildingAttributesWhileSy
569609
}
570610

571611
@Test
572-
void onPreparedProposersUpdated_shouldSendNewNotificationWhenProposerAdded() {
612+
void onPreparedProposersUpdated_shouldNotCallForkChoiceUpdated() {
573613
final ForkChoiceState forkChoiceState = getCurrentForkChoiceState();
574614
final BeaconState headState = getHeadState();
575615
final UInt64 blockSlot = headState.getSlot().plus(1);
576616

577617
notifyForkChoiceUpdated(forkChoiceState);
578618
verify(executionLayerChannel).engineForkChoiceUpdated(forkChoiceState, Optional.empty());
579619

580-
final PayloadBuildingAttributes payloadBuildingAttributes =
581-
withProposerForSlot(forkChoiceState, headState, blockSlot);
582-
verify(executionLayerChannel)
583-
.engineForkChoiceUpdated(forkChoiceState, Optional.of(payloadBuildingAttributes));
620+
withProposerForSlot(forkChoiceState, headState, blockSlot);
621+
verifyNoMoreInteractions(executionLayerChannel);
584622
}
585623

586624
@Test
@@ -882,6 +920,17 @@ private void notifyForkChoiceUpdated(final ForkChoiceState forkChoiceState) {
882920
notifyForkChoiceUpdated(forkChoiceState, Optional.empty());
883921
}
884922

923+
private void notifyForkChoiceUpdatedVerifyNoNotification(final ForkChoiceState forkChoiceState) {
924+
notifyForkChoiceUpdated(
925+
forkChoiceState, Optional.empty(), notification -> assertThat(notification).isNull());
926+
}
927+
928+
private void notifyForkChoiceUpdatedVerifyNoNotification(
929+
final ForkChoiceState forkChoiceState, final Optional<UInt64> proposingSlot) {
930+
notifyForkChoiceUpdated(
931+
forkChoiceState, proposingSlot, notification -> assertThat(notification).isNull());
932+
}
933+
885934
private void notifyForkChoiceUpdated(
886935
final ForkChoiceState forkChoiceState, final Optional<UInt64> proposingSlot) {
887936
notifyForkChoiceUpdated(

Diff for: ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/forkchoice/ProposerDataManagerTest.java

+1-23
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,9 @@
3838
import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState;
3939
import tech.pegasys.teku.spec.executionlayer.ExecutionLayerChannel;
4040
import tech.pegasys.teku.spec.util.DataStructureUtil;
41-
import tech.pegasys.teku.statetransition.forkchoice.ProposersDataManager.ProposersDataManagerSubscriber;
4241
import tech.pegasys.teku.storage.client.RecentChainData;
4342

44-
public class ProposerDataManagerTest implements ProposersDataManagerSubscriber {
43+
public class ProposerDataManagerTest {
4544
private final InlineEventThread eventThread = new InlineEventThread();
4645
private final Spec spec = TestSpecFactory.createMinimalBellatrix();
4746
private final Spec specMock = mock(Spec.class);
@@ -65,9 +64,6 @@ public class ProposerDataManagerTest implements ProposersDataManagerSubscriber {
6564

6665
private final BeaconState state = dataStructureUtil.randomBeaconState();
6766

68-
private boolean onValidatorRegistrationsUpdatedCalled = false;
69-
private boolean onPreparedProposerUpdatedCalled = false;
70-
7167
private final UInt64 slot = UInt64.ONE;
7268
private SszList<SignedValidatorRegistration> registrations;
7369
private final SafeFuture<Void> response = new SafeFuture<>();
@@ -80,7 +76,6 @@ void shouldCallRegisterValidator() {
8076
final SafeFuture<Void> updateCall =
8177
proposersDataManager.updateValidatorRegistrations(registrations, slot);
8278

83-
assertThat(onValidatorRegistrationsUpdatedCalled).isFalse();
8479
verify(executionLayerChannel).builderRegisterValidators(registrations, slot);
8580
verifyNoMoreInteractions(executionLayerChannel);
8681

@@ -89,7 +84,6 @@ void shouldCallRegisterValidator() {
8984
assertThat(updateCall).isCompleted();
9085

9186
// final update
92-
assertThat(onValidatorRegistrationsUpdatedCalled).isTrue();
9387
assertRegisteredValidatorsCount(2);
9488
}
9589

@@ -101,14 +95,12 @@ void shouldNotSignalValidatorRegistrationUpdatedOnError() {
10195
final SafeFuture<Void> updateCall =
10296
proposersDataManager.updateValidatorRegistrations(registrations, slot);
10397

104-
assertThat(onValidatorRegistrationsUpdatedCalled).isFalse();
10598
verify(executionLayerChannel).builderRegisterValidators(registrations, slot);
10699

107100
response.completeExceptionally(new RuntimeException("generic error"));
108101

109102
assertThat(updateCall).isCompletedExceptionally();
110103

111-
assertThat(onValidatorRegistrationsUpdatedCalled).isFalse();
112104
verifyNoMoreInteractions(executionLayerChannel);
113105
assertRegisteredValidatorsCount(0);
114106
}
@@ -128,8 +120,6 @@ void shouldSignalAllDataUpdatedAndShouldExpireData() {
128120
dataStructureUtil.randomUInt64(), dataStructureUtil.randomEth1Address())),
129121
slot);
130122

131-
assertThat(onValidatorRegistrationsUpdatedCalled).isTrue();
132-
assertThat(onPreparedProposerUpdatedCalled).isTrue();
133123
assertRegisteredValidatorsCount(2);
134124
assertPreparedProposersCount(1);
135125

@@ -160,8 +150,6 @@ private void prepareRegistrations() {
160150
when(specMock.getValidatorIndex(state, registrations.get(1).getMessage().getPublicKey()))
161151
.thenReturn(Optional.of(1));
162152
when(specMock.getSlotsPerEpoch(any())).thenReturn(spec.getSlotsPerEpoch(slot));
163-
164-
proposersDataManager.subscribeToProposersDataChanges(this);
165153
}
166154

167155
private void assertPreparedProposersCount(final int expectedCount) {
@@ -179,14 +167,4 @@ private void assertRegisteredValidatorsCount(final int expectedCount) {
179167
.getValue("registered_validators");
180168
assertThat(optionalValue).hasValue(expectedCount);
181169
}
182-
183-
@Override
184-
public void onPreparedProposersUpdated() {
185-
onPreparedProposerUpdatedCalled = true;
186-
}
187-
188-
@Override
189-
public void onValidatorRegistrationsUpdated() {
190-
onValidatorRegistrationsUpdatedCalled = true;
191-
}
192170
}

0 commit comments

Comments
 (0)