123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281(*****************************************************************************)(* *)(* Open Source License *)(* Copyright (c) 2023 Nomadic Labs, <contact@nomadic-labs.com> *)(* Copyright (c) 2023 Functori, <contact@functori.com> *)(* *)(* Permission is hereby granted, free of charge, to any person obtaining a *)(* copy of this software and associated documentation files (the "Software"),*)(* to deal in the Software without restriction, including without limitation *)(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *)(* and/or sell copies of the Software, and to permit persons to whom the *)(* Software is furnished to do so, subject to the following conditions: *)(* *)(* The above copyright notice and this permission notice shall be included *)(* in all copies or substantial portions of the Software. *)(* *)(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *)(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *)(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *)(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *)(* DEALINGS IN THE SOFTWARE. *)(* *)(*****************************************************************************)(**
Errors
======
*)typeerror+=Cannot_find_blockofBlock_hash.tlet()=register_error_kind~id:"sc_rollup.node.cannot_find_block"~title:"Cannot find block from L1"~description:"A block couldn't be found from the L1 node"~pp:(funppfhash->Format.fprintfppf"Block with hash %a was not found on the L1 node."Block_hash.pphash)`TemporaryData_encoding.(obj1(req"hash"Block_hash.encoding))(functionCannot_find_blockhash->Somehash|_->None)(funhash->Cannot_find_blockhash)(**
State
=====
*)typeheader={hash:Block_hash.t;level:int32;header:Block_header.shell_header;}letheader_encoding=letopenData_encodinginconv(fun{hash;level=_;header}->(hash,header))(fun(hash,header)->{hash;level=header.level;header})(merge_objs(obj1(req"hash"Block_hash.encoding))Block_header.shell_header_encoding)typehead={hash:Block_hash.t;level:int32}lethead_encoding=letopenData_encodinginconv(fun{hash;level}->(hash,level))(fun(hash,level)->{hash;level})(obj2(req"hash"Block_hash.encoding)(req"level"Data_encoding.int32))lethead_of_header{hash;level;header=_}={hash;level}moduleBlocks_cache=Aches_lwt.Lache.Make_result(Aches.Rache.Transfer(Aches.Rache.LRU)(Block_hash))typeblock=..typefetch_block_rpc=Client_context.full->?metadata:[`Always|`Never]->?chain:Tezos_shell_services.Block_services.chain->?block:Tezos_shell_services.Block_services.block->unit->blocktzresultLwt.ttypeheaders_cache=(Block_header.shell_header,tztrace)Blocks_cache.ttypeblocks_cache=(block,tztrace)Blocks_cache.topenOctez_crawler.Layer_1typenonrect={l1:t;cctxt:Client_context.full;blocks_cache:blocks_cache;(** Global blocks cache for the smart rollup node. *)headers_cache:headers_cache;(** Global block headers cache for the smart rollup node. *)prefetch_blocks:int;(** Number of blocks to prefetch by default. *)}letstart~name~reconnection_delay~l1_blocks_cache_size?protocols?(prefetch_blocks=l1_blocks_cache_size)cctxt=letopenLwt_result_syntaxinlet*?()=ifprefetch_blocks>l1_blocks_cache_sizethenerror_with"Blocks to prefetch must be less than the cache size: %d"l1_blocks_cache_sizeelseOk()inlet*!l1=start~name~reconnection_delay?protocolscctxtinletblocks_cache=Blocks_cache.createl1_blocks_cache_sizeinletheaders_cache=Blocks_cache.createl1_blocks_cache_sizeinletcctxt=(cctxt:>Client_context.full)inreturn{l1;cctxt;blocks_cache;headers_cache;prefetch_blocks}letshutdown{l1;_}=shutdownl1letcache_shell_header{headers_cache;_}hashheader=Blocks_cache.putheaders_cachehash(Lwt.return_okheader)letclient_context{cctxt;_}=cctxtletiter_headsl1_ctxtf=iter_headsl1_ctxt.l1@@fun(hash,{shell={level;_}asheader;_})->cache_shell_headerl1_ctxthashheader;f{hash;level;header}letwait_firstl1_ctxt=letopenLwt_syntaxinlet+hash,{shell={level;_}asheader;_}=wait_firstl1_ctxt.l1in{hash;level;header}letget_predecessor_opt?max_read{l1;_}=get_predecessor_opt?max_readl1letget_predecessor?max_read{l1;_}=get_predecessor?max_readl1letget_tezos_reorg_for_new_head{l1;_}?get_old_predecessorold_headnew_head=get_tezos_reorg_for_new_headl1?get_old_predecessorold_headnew_headmoduleInternal_for_tests=structletdummycctxt={l1=Internal_for_tests.dummycctxt;cctxt=(cctxt:>Client_context.full);blocks_cache=Blocks_cache.create1;headers_cache=Blocks_cache.create1;prefetch_blocks=0;}end(**
Helpers
=======
*)(** [fetch_tezos_block cctxt hash] returns a block shell header of
[hash]. Looks for the block in the blocks cache first, and fetches it from
the L1 node otherwise. *)letfetch_tezos_shell_header{cctxt;headers_cache;_}hash=trace(Cannot_find_blockhash)@@letfetchhash=Tezos_shell_services.Shell_services.Blocks.Header.shell_headercctxt~chain:`Main~block:(`Hash(hash,0))()inBlocks_cache.bind_or_putheaders_cachehashfetchLwt.returnletfetch_block_no_cache(fetch:fetch_block_rpc)extract_header({cctxt;blocks_cache;_}asl1_ctxt)hash=letopenLwt_result_syntaxintrace(Cannot_find_blockhash)@@let*block=fetchcctxt~chain:`Main~block:(`Hash(hash,0))~metadata:`Always()inBlocks_cache.putblocks_cachehash(Lwt.return_okblock);cache_shell_headerl1_ctxthash(extract_headerblock);returnblock(** [fetch_tezos_block cctxt fetch extract_header hash] returns a block info
given a block hash. Looks for the block in the blocks cache first, and
fetches it from the L1 node otherwise. *)letfetch_tezos_block(fetch_rpc:fetch_block_rpc)extract_header({cctxt;blocks_cache;_}asl1_ctxt)hash=trace(Cannot_find_blockhash)@@letopenLwt_result_syntaxinletfetchhash=let*block=fetch_rpccctxt~chain:`Main~block:(`Hash(hash,0))~metadata:`Always()incache_shell_headerl1_ctxthash(extract_headerblock);returnblockinlet*!block=Blocks_cache.bind_or_putblocks_cachehashfetchLwt.returnin(* TODO: https://gitlab.com/tezos/tezos/-/issues/6292
Consider cleaner ways to "prefetch" tezos blocks:
- know before where are protocol boundaries
- prefetch blocks in binary form *)letis_of_expected_protocol=matchblockwith|Error(Tezos_rpc_http.RPC_client_errors.(Request_failed{error=Unexpected_content_;_})::_)->(* The promise cached failed to parse the block because it was for the
wrong protocol. *)false|Error_->(* The promise cached failed for another reason. *)true|Okblock->((* We check if we are able to extract the header, which inherently
ensures that we are in the correct case of type {!type:block}. *)trylet(_:Block_header.shell_header)=extract_headerblockintruewith_->(* This can happen if the blocks for two protocols have the same
binary representation. *)false)inifis_of_expected_protocolthenLwt.returnblockelse(* It is possible for a value stored in the cache to have been parsed
with the wrong protocol code because:
1. There is no protocol information in binary blocks
2. We pre-fetch blocks eagerly.
If we realize a posteriori that the block we cached is for another
protocol then we overwrite it with the correctly parsed one.
*)fetch_block_no_cachefetch_rpcextract_headerl1_ctxthashletmake_prefetching_schedule{prefetch_blocks;_}blocks=letblocks_with_prefetching,_,first_prefetch=List.fold_left(fun(acc,nb_prefetch,prefetch)b->letnb_prefetch=nb_prefetch+1inletprefetch=b::prefetchinifnb_prefetch>=prefetch_blocksthen((b,prefetch)::acc,0,[])else((b,[])::acc,nb_prefetch,prefetch))([],0,[])(List.revblocks)inmatch(blocks_with_prefetching,first_prefetch)with|[],_|_,[]->blocks_with_prefetching|(first,_)::rest,_->(first,first_prefetch)::restletprefetch_tezos_blocksfetchextract_headerl1_ctxt=function|[]->()|blocks->Lwt.async@@fun()->List.iter_p(fun{hash;_}->letopenLwt_syntaxinlet+_maybe_block=fetch_tezos_blockfetchextract_headerl1_ctxthashin())blocks