@@ -722,8 +722,8 @@ describe("UnknownBlockSync", () => {
722722
723723 beforeEach ( ( ) => {
724724 vi . useFakeTimers ( { shouldAdvanceTime : true } ) ;
725- vi . mocked ( validateGossipExecutionPayloadEnvelope ) . mockClear ( ) ;
726- vi . mocked ( validateGloasBlockDataColumnSidecars ) . mockClear ( ) ;
725+ vi . mocked ( validateGossipExecutionPayloadEnvelope ) . mockReset ( ) . mockResolvedValue ( undefined ) ;
726+ vi . mocked ( validateGloasBlockDataColumnSidecars ) . mockReset ( ) . mockResolvedValue ( undefined ) ;
727727 } ) ;
728728
729729 it ( "fetches and processes unknown envelope by root when payload input exists" , async ( ) => {
@@ -938,6 +938,107 @@ describe("UnknownBlockSync", () => {
938938 expect ( processExecutionPayload ) . toHaveBeenCalledWith ( payloadInput ) ;
939939 } ) ;
940940
941+ it ( "defers envelope validation until the block is in fork choice when payload input is seeded from the block body" , async ( ) => {
942+ const peer = await getRandPeerIdStr ( ) ;
943+ const { block, blockRoot, blockRootHex, payloadInput, envelope} = buildPayloadFixture ( {
944+ blobCount : 0 ,
945+ sampledColumns : [ ] ,
946+ slot : 1 ,
947+ } ) ;
948+ const parentRootHex = toRootHex ( block . message . parentRoot ) ;
949+
950+ // payloadInput is seeded from the block body during download, so the cache returns it before the block
951+ // is imported into fork choice. Block becomes known only once processBlock imports it.
952+ let blockImported = false ;
953+ const knownRoots = new Set ( [ parentRootHex ] ) ;
954+
955+ const sendExecutionPayloadEnvelopesByRoot = vi . fn ( ) . mockResolvedValue ( [ envelope ] ) ;
956+ const sendBeaconBlocksByRoot = vi . fn ( ) . mockResolvedValue ( [ block ] ) ;
957+ const processExecutionPayload = vi . fn ( ) . mockResolvedValue ( undefined ) ;
958+
959+ let emitter ! : ChainEventEmitter ;
960+ const processBlock = vi . fn ( ) . mockImplementation ( async ( ) => {
961+ blockImported = true ;
962+ knownRoots . add ( blockRootHex ) ;
963+ emitter . emit ( routes . events . EventType . block , { slot : 1 , block : blockRootHex , executionOptimistic : false } ) ;
964+ } ) ;
965+
966+ // Reproduce BLOCK_ROOT_UNKNOWN: validation rejects while the block is absent from fork choice. The fix must
967+ // not call it until the block is imported.
968+ vi . mocked ( validateGossipExecutionPayloadEnvelope ) . mockImplementation ( async ( ) => {
969+ if ( ! blockImported ) {
970+ throw new Error ( "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BLOCK_ROOT_UNKNOWN" ) ;
971+ }
972+ } ) ;
973+
974+ ( { emitter} = setupPayloadSyncTest ( {
975+ chainOverrides : {
976+ processBlock,
977+ processExecutionPayload,
978+ seenPayloadEnvelopeInputCache : {
979+ add : vi . fn ( ) ,
980+ get : vi . fn ( ) . mockImplementation ( ( root : string ) => ( root === blockRootHex ? payloadInput : undefined ) ) ,
981+ prune : vi . fn ( ) ,
982+ } as unknown as IBeaconChain [ "seenPayloadEnvelopeInputCache" ] ,
983+ seenBlockInputCache : {
984+ getByBlock : ( {
985+ block,
986+ blockRootHex,
987+ seenTimestampSec,
988+ source,
989+ } : {
990+ block : gloas . SignedBeaconBlock ;
991+ blockRootHex : string ;
992+ seenTimestampSec : number ;
993+ source : BlockInputSource ;
994+ } ) =>
995+ createGloasBlockInput ( {
996+ block,
997+ blockRootHex,
998+ seenTimestampSec,
999+ source,
1000+ } ) ,
1001+ prune : vi . fn ( ) ,
1002+ } as unknown as SeenBlockInput ,
1003+ forkChoice : {
1004+ hasPayloadHexUnsafe : vi . fn ( ) . mockReturnValue ( false ) ,
1005+ hasBlockHex : vi . fn ( ) . mockImplementation ( ( root : string ) => knownRoots . has ( root ) ) ,
1006+ getBlockHexAndBlockHash : vi
1007+ . fn ( )
1008+ . mockImplementation ( ( root : string , hash : string ) =>
1009+ root === parentRootHex &&
1010+ hash === toRootHex ( block . message . body . signedExecutionPayloadBid . message . parentBlockHash )
1011+ ? ( { slot : 0 } as ProtoBlock )
1012+ : null
1013+ ) ,
1014+ getFinalizedBlock : vi . fn ( ) . mockReturnValue ( { slot : 0 } as ProtoBlock ) ,
1015+ } as unknown as IForkChoice ,
1016+ } ,
1017+ networkOverrides : {
1018+ sendExecutionPayloadEnvelopesByRoot,
1019+ sendBeaconBlocksByRoot,
1020+ } ,
1021+ peers : [ { peerId : peer } ] ,
1022+ } ) ) ;
1023+
1024+ emitter . emit ( ChainEvent . unknownEnvelopeBlockRoot , {
1025+ rootHex : blockRootHex ,
1026+ peer,
1027+ source : BlockInputSource . gossip ,
1028+ } ) ;
1029+
1030+ await sleep ( 80 ) ;
1031+
1032+ // Envelope downloaded, block pulled because validation was deferred, then envelope validated and processed
1033+ // only after the block landed in fork choice.
1034+ expect ( sendExecutionPayloadEnvelopesByRoot ) . toHaveBeenCalledWith ( peer , [ blockRoot ] ) ;
1035+ expect ( sendBeaconBlocksByRoot ) . toHaveBeenCalledWith ( peer , [ blockRoot ] ) ;
1036+ expect ( processBlock ) . toHaveBeenCalledTimes ( 1 ) ;
1037+ expect ( validateGossipExecutionPayloadEnvelope ) . toHaveBeenCalledOnce ( ) ;
1038+ expect ( processExecutionPayload ) . toHaveBeenCalledTimes ( 1 ) ;
1039+ expect ( processExecutionPayload ) . toHaveBeenCalledWith ( payloadInput ) ;
1040+ } ) ;
1041+
9411042 it ( "downloads the block and retries payload import when EL reports block not in fork choice" , async ( ) => {
9421043 const peer = await getRandPeerIdStr ( ) ;
9431044 const { block, blockRoot, blockRootHex, payloadInput, envelope} = buildPayloadFixture ( {
0 commit comments