Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use header based selector with slot fallback #8522

Merged
merged 4 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import static org.mockito.Mockito.when;
import static tech.pegasys.teku.beaconrestapi.v1.validator.PostBlindedAndUnblindedBlockTest.Version.V1;
import static tech.pegasys.teku.beaconrestapi.v1.validator.PostBlindedAndUnblindedBlockTest.Version.V2;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_BAD_REQUEST;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK;

import java.io.IOException;
Expand Down Expand Up @@ -58,37 +59,22 @@ public static Stream<Arguments> postBlockCases() {
.flatMap(
route ->
Stream.of(
// route, useSsz, versionHeader
Arguments.of(route, false, Optional.empty()),
// Methods using Eth-Consensus-Version header (only for SSZ, we could add it for
// json too when we start using it)
Arguments.of(route, true, Optional.empty()),

// deneb header on bellatrix will fall back to ssz slot based selector, because
// deneb is not configured.
// It is irrelevant for json, because we currently always use the slot based
// selector.
Arguments.of(route, true, Optional.of("deneb"))))
// route, useSsz
Arguments.of(route, false), Arguments.of(route, true)))
.map(
args -> {
final String route = (String) args.get()[0];
final boolean isBlindedBlock = route.contains("blinded");
final Version version = route.contains("/v2/") ? V2 : V1;
final boolean useSsz = (boolean) args.get()[1];
@SuppressWarnings("unchecked")
final Optional<String> versionHeader = (Optional<String>) args.get()[2];
return Arguments.of(version, isBlindedBlock, route, useSsz, versionHeader);
return Arguments.of(version, isBlindedBlock, route, useSsz);
});
}

@ParameterizedTest(name = "version:{0}_blinded:{1}_ssz:{3}_versionHeader:{4}")
@ParameterizedTest(name = "version:{0}_blinded:{1}_ssz:{3}")
@MethodSource("postBlockCases")
void shouldReturnOk(
final Version version,
final boolean isBlindedBlock,
final String route,
final boolean useSsz,
final Optional<String> versionHeader)
void preDeneb(
final Version version, final boolean isBlindedBlock, final String route, final boolean useSsz)
throws IOException {

startRestAPIAtGenesis(SpecMilestone.BELLATRIX);
Expand All @@ -111,17 +97,21 @@ void shouldReturnOk(

prepareResponse(request, version);

postRequestAndAssert(route, request, signedBeaconBlockSchema, versionHeader, useSsz, version);
postRequestAndAssert(
route,
request,
signedBeaconBlockSchema,
Optional.of(SpecMilestone.BELLATRIX.name()),
useSsz,
version);
postRequestAndAssert(
route, request, signedBeaconBlockSchema, Optional.empty(), useSsz, version);
}

@ParameterizedTest(name = "version:{0}_blinded:{1}_ssz:{3}_versionHeader:{4}")
@ParameterizedTest(name = "version:{0}_blinded:{1}_ssz:{3}")
@MethodSource("postBlockCases")
void shouldReturnOkPostDeneb(
final Version version,
final boolean isBlindedBlock,
final String route,
final boolean useSsz,
final Optional<String> versionHeader)
void postDeneb(
final Version version, final boolean isBlindedBlock, final String route, final boolean useSsz)
throws IOException {
startRestAPIAtGenesis(SpecMilestone.DENEB);
dataStructureUtil = new DataStructureUtil(spec);
Expand All @@ -144,7 +134,14 @@ void shouldReturnOkPostDeneb(
prepareResponse(request, version);

postRequestAndAssert(
route, request, signedBlockContainerSchema, versionHeader, useSsz, version);
route,
request,
signedBlockContainerSchema,
Optional.of(SpecMilestone.DENEB.name()),
useSsz,
version);
postRequestAndAssert(
route, request, signedBlockContainerSchema, Optional.empty(), useSsz, version);
}

private void prepareResponse(final SignedBeaconBlock request, final Version version) {
Expand Down Expand Up @@ -207,7 +204,14 @@ private <T extends SszData> void postRequestAndAssert(
JsonUtil.serialize(request, signedBlockContainerSchema.getJsonTypeDefinition()),
params,
versionHeader)) {
assertThat(response.code()).isEqualTo(SC_OK);
if (version == V2 && versionHeader.isEmpty()) {
// the version header is required in V2 APIs
assertThat(response.code()).isEqualTo(SC_BAD_REQUEST);
} else {
// the version header is not required for V1 APIs, the header selector should fall back to
// the slot selector
assertThat(response.code()).isEqualTo(SC_OK);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import tech.pegasys.teku.beaconrestapi.AbstractDataBackedRestAPIIntegrationTest;
import tech.pegasys.teku.beaconrestapi.handlers.v2.beacon.PostAttestationsV2;
import tech.pegasys.teku.infrastructure.async.SafeFuture;
import tech.pegasys.teku.infrastructure.http.RestApiConstants;
import tech.pegasys.teku.infrastructure.json.JsonTestUtil;
import tech.pegasys.teku.infrastructure.json.JsonUtil;
import tech.pegasys.teku.infrastructure.json.types.SerializableTypeDefinition;
Expand Down Expand Up @@ -162,6 +161,6 @@ void shouldFailWhenBadConsensusHeaderValue() throws Exception {
.isEqualTo(
String.format(
"Invalid value for (%s) header: %s",
RestApiConstants.HEADER_CONSENSUS_VERSION, badConsensusHeaderValue));
HEADER_CONSENSUS_VERSION, badConsensusHeaderValue));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
import tech.pegasys.teku.beaconrestapi.AbstractDataBackedRestAPIIntegrationTest;
import tech.pegasys.teku.beaconrestapi.handlers.v2.beacon.PostAttesterSlashingV2;
import tech.pegasys.teku.infrastructure.async.SafeFuture;
import tech.pegasys.teku.infrastructure.http.RestApiConstants;
import tech.pegasys.teku.infrastructure.json.JsonTestUtil;
import tech.pegasys.teku.spec.SpecMilestone;
import tech.pegasys.teku.spec.TestSpecContext;
Expand Down Expand Up @@ -156,6 +155,6 @@ void shouldFailWhenBadConsensusHeaderValue() throws Exception {
.isEqualTo(
String.format(
"Invalid value for (%s) header: %s",
RestApiConstants.HEADER_CONSENSUS_VERSION, badConsensusHeaderValue));
HEADER_CONSENSUS_VERSION, badConsensusHeaderValue));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,16 @@ SerializableOneOfTypeDefinition<T> getMultipleSchemaDefinitionFromMilestone(
return builder.build();
}

public static <T extends SszData> DeserializableTypeDefinition<? extends T> slotBasedSelector(
final String json,
final SchemaDefinitionCache schemaDefinitionCache,
final Function<SchemaDefinitions, SszSchema<? extends T>> getSchema) {
final Optional<UInt64> slot =
// SignedBeaconBlock
getSlot(json, "message", "slot")
// SignedBlockContents
.or(() -> getSlot(json, "signed_block", "message", "slot"));
final SpecMilestone milestone =
schemaDefinitionCache.milestoneAtSlot(
slot.orElseThrow(() -> new BadRequestException("Could not locate slot in JSON data")));
return getSchema
.apply(schemaDefinitionCache.getSchemaDefinition(milestone))
.getJsonTypeDefinition();
public static <T extends SszData>
DeserializableTypeDefinition<? extends T> headerBasedSelectorWithSlotFallback(
final Map<String, String> headers,
final String json,
final SchemaDefinitionCache schemaDefinitionCache,
final Function<SchemaDefinitions, SszSchema<? extends T>> getSchema) {
if (headers.containsKey(HEADER_CONSENSUS_VERSION)) {
return headerBasedSelector(headers, schemaDefinitionCache, getSchema);
}
return slotBasedSelector(json, schemaDefinitionCache, getSchema);
}

public static <T extends SszData> DeserializableTypeDefinition<? extends T> headerBasedSelector(
Expand All @@ -117,6 +112,23 @@ public static <T extends SszData> DeserializableTypeDefinition<? extends T> head
}
}

private static <T extends SszData> DeserializableTypeDefinition<? extends T> slotBasedSelector(
final String json,
final SchemaDefinitionCache schemaDefinitionCache,
final Function<SchemaDefinitions, SszSchema<? extends T>> getSchema) {
final Optional<UInt64> slot =
// SignedBeaconBlock
getSlot(json, "message", "slot")
// SignedBlockContents
.or(() -> getSlot(json, "signed_block", "message", "slot"));
Comment on lines +122 to +123
Copy link
Contributor

Choose a reason for hiding this comment

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

is block contents still a thing? I thought that went away?

Copy link
Contributor Author

@mehdi-aouadi mehdi-aouadi Aug 22, 2024

Choose a reason for hiding this comment

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

Yep, we added them since Deneb to handle block and blobs. thought we could remove this use case but turned out necessary when we send Deneb block contents to the post block V1 apis, they might retrieve the slot in order to get the correct schema

final SpecMilestone milestone =
schemaDefinitionCache.milestoneAtSlot(
slot.orElseThrow(() -> new BadRequestException("Could not locate slot in JSON data")));
return getSchema
.apply(schemaDefinitionCache.getSchemaDefinition(milestone))
.getJsonTypeDefinition();
}

private static Optional<UInt64> getSlot(final String json, final String... path) {
try {
return JsonUtil.getAttribute(json, CoreTypes.UINT64_TYPE, path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.ETH_CONSENSUS_VERSION_TYPE;
import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.MilestoneDependentTypesUtil.getSchemaDefinitionForAllSupportedMilestones;
import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.MilestoneDependentTypesUtil.slotBasedSelector;
import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.MilestoneDependentTypesUtil.headerBasedSelectorWithSlotFallback;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_ACCEPTED;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_SERVICE_UNAVAILABLE;
Expand Down Expand Up @@ -100,7 +100,8 @@ broadcast but a different status code is returned (202). Pre-Bellatrix, this end
.milestoneAtSlot(blockContainer.getSlot())
.equals(milestone)),
context ->
slotBasedSelector(
headerBasedSelectorWithSlotFallback(
context.getHeaders(),
context.getBody(),
schemaDefinitionCache,
SchemaDefinitions::getSignedBlindedBlockContainerSchema),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.ETH_CONSENSUS_VERSION_TYPE;
import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.MilestoneDependentTypesUtil.getSchemaDefinitionForAllSupportedMilestones;
import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.MilestoneDependentTypesUtil.slotBasedSelector;
import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.MilestoneDependentTypesUtil.headerBasedSelectorWithSlotFallback;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_ACCEPTED;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_SERVICE_UNAVAILABLE;
Expand Down Expand Up @@ -100,7 +100,8 @@ validation, a separate success response code (202) is used to indicate that the
.milestoneAtSlot(blockContainer.getSlot())
.equals(milestone)),
context ->
slotBasedSelector(
headerBasedSelectorWithSlotFallback(
context.getHeaders(),
context.getBody(),
schemaDefinitionCache,
SchemaDefinitions::getSignedBlockContainerSchema),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.ETH_CONSENSUS_VERSION_TYPE;
import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.PARAMETER_BROADCAST_VALIDATION;
import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.MilestoneDependentTypesUtil.getSchemaDefinitionForAllSupportedMilestones;
import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.MilestoneDependentTypesUtil.slotBasedSelector;
import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.MilestoneDependentTypesUtil.headerBasedSelector;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_ACCEPTED;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_SERVICE_UNAVAILABLE;
Expand Down Expand Up @@ -113,8 +113,8 @@ broadcast but a different status code is returned (202). Pre-Bellatrix, this end
.milestoneAtSlot(blockContainer.getSlot())
.equals(milestone)),
context ->
slotBasedSelector(
context.getBody(),
headerBasedSelector(
context.getHeaders(),
schemaDefinitionCache,
SchemaDefinitions::getSignedBlindedBlockContainerSchema),
spec::deserializeSignedBlindedBlockContainer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.ETH_CONSENSUS_VERSION_TYPE;
import static tech.pegasys.teku.beaconrestapi.BeaconRestApiTypes.PARAMETER_BROADCAST_VALIDATION;
import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.MilestoneDependentTypesUtil.getSchemaDefinitionForAllSupportedMilestones;
import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.MilestoneDependentTypesUtil.slotBasedSelector;
import static tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.MilestoneDependentTypesUtil.headerBasedSelector;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_ACCEPTED;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_OK;
import static tech.pegasys.teku.infrastructure.http.HttpStatusCodes.SC_SERVICE_UNAVAILABLE;
Expand Down Expand Up @@ -113,8 +113,8 @@ validation, a separate success response code (202) is used to indicate that the
.milestoneAtSlot(blockContainer.getSlot())
.equals(milestone)),
context ->
slotBasedSelector(
context.getBody(),
headerBasedSelector(
context.getHeaders(),
schemaDefinitionCache,
SchemaDefinitions::getSignedBlockContainerSchema),
spec::deserializeSignedBlockContainer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@

import java.util.Collections;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import tech.pegasys.teku.api.exceptions.BadRequestException;
import tech.pegasys.teku.beaconrestapi.handlers.v1.beacon.MilestoneDependentTypesUtil;
import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition;
Expand All @@ -33,34 +37,20 @@ public class MilestoneDependentTypesUtilTest {
private final Spec spec = TestSpecFactory.createMinimalElectra();
private final SchemaDefinitionCache cache = new SchemaDefinitionCache(spec);

@Test
void headerSelector_UsesConsensusVersionPhase0() {
@ParameterizedTest(name = "{0}")
@MethodSource("milestones")
void headerSelector_UsesConsensusVersionPhase0(final SpecMilestone targetMilestone) {
final DeserializableTypeDefinition<?> typeDefinition =
MilestoneDependentTypesUtil.headerBasedSelector(
Map.of(HEADER_CONSENSUS_VERSION, SpecMilestone.PHASE0.toString()),
Map.of(HEADER_CONSENSUS_VERSION, targetMilestone.toString()),
cache,
SchemaDefinitions::getAttestationSchema);
assertThat(typeDefinition.getTypeName()).contains("AttestationPhase0");
}

@Test
void headerSelector_UsesConsensusVersionDeneb() {
final DeserializableTypeDefinition<?> typeDefinition =
MilestoneDependentTypesUtil.headerBasedSelector(
Map.of(HEADER_CONSENSUS_VERSION, SpecMilestone.DENEB.toString()),
cache,
SchemaDefinitions::getAttestationSchema);
assertThat(typeDefinition.getTypeName()).contains("AttestationPhase0");
}

@Test
void headerSelector_UsesConsensusVersionElectra() {
final DeserializableTypeDefinition<?> typeDefinition =
MilestoneDependentTypesUtil.headerBasedSelector(
Map.of(HEADER_CONSENSUS_VERSION, SpecMilestone.ELECTRA.toString()),
cache,
SchemaDefinitions::getAttestationSchema);
assertThat(typeDefinition.getTypeName()).contains("AttestationElectra");
assertThat(typeDefinition.getTypeName())
.contains(
spec.forMilestone(targetMilestone)
.getSchemaDefinitions()
.getAttestationSchema()
.getContainerName());
}

@Test
Expand All @@ -84,4 +74,11 @@ void headerSelector_errorsWhenMissing() {
.isInstanceOf(BadRequestException.class)
.hasMessageContaining("Missing required header value for (Eth-Consensus-Version)");
}

private static Stream<Arguments> milestones() {
return Stream.of(
Arguments.of(SpecMilestone.PHASE0),
Arguments.of(SpecMilestone.DENEB),
Arguments.of(SpecMilestone.ELECTRA));
}
}