@@ -6,7 +6,7 @@ use axum::http::HeaderValue;
66use bytes:: Bytes ;
77use futures:: StreamExt ;
88use headers_accept:: Accept ;
9- use lh_types:: { BeaconBlock , ForkName } ;
9+ use lh_types:: { BeaconBlock , ForkName , SignedBeaconBlock , map_fork_name } ;
1010use mediatype:: { MediaType , ReadParams , names} ;
1111use reqwest:: {
1212 Response ,
@@ -403,8 +403,22 @@ pub fn deserialize_body(
403403 } ;
404404
405405 match encoding {
406- EncodingType :: Json => serde_json:: from_slice :: < SignedBlindedBeaconBlock > ( & body)
407- . map_err ( BodyDeserializeError :: SerdeJsonError ) ,
406+ EncodingType :: Json => match get_consensus_version_header ( headers) {
407+ // `SignedBlindedBeaconBlock` is untagged and Electra and Fulu are
408+ // field-identical, so a plain `from_slice` stops at Electra and
409+ // reports the wrong fork for every Fulu block.
410+ Some ( version) => Ok ( map_fork_name ! (
411+ version,
412+ SignedBeaconBlock ,
413+ serde_json:: from_slice( & body) . map_err( BodyDeserializeError :: SerdeJsonError ) ?
414+ ) ) ,
415+ // builder-specs doesn't require the header for JSON bodies.
416+ // A request without it still has to decode and an untagged decode would silently pick
417+ // Electra. Assume Fulu to be conservative until ePBS warrants the refactor
418+ None => Ok ( SignedBeaconBlock :: Fulu (
419+ serde_json:: from_slice ( & body) . map_err ( BodyDeserializeError :: SerdeJsonError ) ?,
420+ ) ) ,
421+ } ,
408422 EncodingType :: Ssz => match get_consensus_version_header ( headers) {
409423 Some ( version) => SignedBlindedBeaconBlock :: from_ssz_bytes_with ( & body, |bytes| {
410424 BeaconBlock :: from_ssz_bytes_for_fork ( bytes, version)
@@ -930,4 +944,62 @@ mod test {
930944 let err = deserialize_body ( & headers, body) . unwrap_err ( ) ;
931945 assert ! ( matches!( err, BodyDeserializeError :: MissingVersionHeader ) ) ;
932946 }
947+
948+ /// A blinded block body, encoded as JSON, with the given consensus version.
949+ ///
950+ /// The Electra fixture is deliberately reused for the Fulu case: Electra
951+ /// and Fulu have *identical* `BeaconBlockBody` fields, and their
952+ /// `ExecutionPayloadHeader`s are identical too, so one byte string is a
953+ /// valid encoding of BOTH forks. That ambiguity is the whole point - the
954+ /// header is the only thing that can tell them apart.
955+ fn blinded_block_json ( version : Option < & ' static str > ) -> ( HeaderMap , Bytes ) {
956+ let body = Bytes :: from_static ( include_bytes ! (
957+ "pbs/types/testdata/signed-blinded-beacon-block-electra.json"
958+ ) ) ;
959+ let mut headers = HeaderMap :: new ( ) ;
960+ headers. insert ( CONTENT_TYPE , HeaderValue :: from_static ( APPLICATION_JSON ) ) ;
961+ if let Some ( version) = version {
962+ headers. insert (
963+ HeaderName :: try_from ( CONSENSUS_VERSION_HEADER ) . unwrap ( ) ,
964+ HeaderValue :: from_static ( version) ,
965+ ) ;
966+ }
967+ ( headers, body)
968+ }
969+
970+ /// A JSON body labelled `fulu` must decode AS Fulu.
971+ #[ test]
972+ fn test_deserialize_body_json_decodes_fulu_as_fulu ( ) {
973+ let ( headers, body) = blinded_block_json ( Some ( "fulu" ) ) ;
974+ let block = deserialize_body ( & headers, body) . expect ( "fulu body decodes" ) ;
975+ assert_eq ! (
976+ block. fork_name_unchecked( ) ,
977+ ForkName :: Fulu ,
978+ "decoded fork must follow Eth-Consensus-Version, not the untagged variant scan"
979+ ) ;
980+ }
981+
982+ /// The same bytes labelled `electra` must still decode as Electra
983+ /// The header is honoured in both directions
984+ #[ test]
985+ fn test_deserialize_body_json_decodes_electra_as_electra ( ) {
986+ let ( headers, body) = blinded_block_json ( Some ( "electra" ) ) ;
987+ let block = deserialize_body ( & headers, body) . expect ( "electra body decodes" ) ;
988+ assert_eq ! ( block. fork_name_unchecked( ) , ForkName :: Electra ) ;
989+ }
990+
991+ /// The header is `required: false` for JSON requests in builder-specs
992+ /// ("Required if request is SSZ encoded"), so a body without it must still
993+ /// decode. It is assumed to be Fulu rather than left to the untagged scan,
994+ /// which would silently pick Electra.
995+ #[ test]
996+ fn test_deserialize_body_json_without_version_header_assumes_fulu ( ) {
997+ let ( headers, body) = blinded_block_json ( None ) ;
998+ let block = deserialize_body ( & headers, body) . expect ( "decodes without the header" ) ;
999+ assert_eq ! (
1000+ block. fork_name_unchecked( ) ,
1001+ ForkName :: Fulu ,
1002+ "a headerless JSON body must not fall back to the untagged Electra match"
1003+ ) ;
1004+ }
9331005}
0 commit comments