LCOV - code coverage report
Current view: top level - flamenco/runtime/tests - fd_shred_harness.c (source / functions) Hit Total Coverage
Test: cov.lcov Lines: 0 250 0.0 %
Date: 2026-07-17 05:22:26 Functions: 0 3 0.0 %

          Line data    Source code
       1             : #include "fd_solfuzz_private.h"
       2             : #include "generated/shred.pb.h"
       3             : 
       4             : #include "../../../ballet/shred/fd_shred.h"
       5             : #include "../../../disco/shred/fd_fec_resolver.h"
       6             : #include "../../../disco/metrics/fd_metrics.h"
       7             : #include "../../../discof/reasm/fd_reasm.h"
       8             : #include "../../../discof/replay/fd_sched.h"
       9             : 
      10             : /* Resolver sizing.  Production sizing lives in the shred tile setup;
      11             :    the values below are much smaller because the harness consumes
      12             :    results synchronously, so partial/complete queue depth has no
      13             :    consumer-lag to absorb.  See fd_fec_resolver.h for the precise
      14             :    semantics of each depth parameter:
      15             :      - DEPTH:          max in-flight FEC sets before spill
      16             :      - PARTIAL_DEPTH:  out_shred pointer-lifetime guarantee
      17             :      - COMPLETE_DEPTH: out_fec_set pointer-lifetime guarantee
      18             :      - DONE_DEPTH:     memory of completed (slot, fec_set_idx)
      19             :                        pairs used for duplicate detection */
      20             : static ulong const RESOLVER_DEPTH          = 32UL;
      21             : static ulong const RESOLVER_PARTIAL_DEPTH  = 8UL;
      22             : static ulong const RESOLVER_COMPLETE_DEPTH = 8UL;
      23             : static ulong const RESOLVER_DONE_DEPTH     = 256UL;
      24             : 
      25             : /* Reasm + scheduler sizing.  Only loosely tied to the resolver depths
      26             :    above: large enough to cover any reasonable fuzz input while keeping
      27             :    spad footprint bounded. */
      28             : static ulong const REASM_POOL_MAX       = 1024UL;
      29             : 
      30             : /* Scheduler txn/block pool sizes, bounded to the fuzz input so
      31             :    fd_sched_new's per-run free-list init stays cheap. */
      32             : static ulong const SCHED_DEPTH          = 4096UL;
      33             : static ulong const SCHED_BLOCK_CNT_MAX  = 128UL;
      34             : static ulong const SCHED_EXEC_CNT       = 4UL;
      35             : 
      36             : /* Tick-verification parameters passed to fd_sched_block_verify_ticks.
      37             :    ticks_per_slot=64 (tick_height 0, max_tick_height 64) and
      38             :    hashes_per_tick=62500 to match mainnet and solfuzz-agave's reference
      39             :    bank. */
      40             : static ulong const SLOT_MAX_TICK_HEIGHT = 64UL;
      41             : static ulong const SLOT_HASHES_PER_TICK = 62500UL;
      42             : 
      43             : typedef struct {
      44             :   fd_hash_t mr;
      45             :   fd_hash_t cmr;
      46             :   ulong     slot;
      47             :   uint      fec_set_idx;
      48             :   ushort    parent_off;
      49             :   ushort    num_data_shreds;
      50             :   ushort    num_coding_shreds;
      51             :   uint      shred_cnt;
      52             :   uint      shred_offs[ FD_FEC_SHRED_CNT ];
      53             :   int       data_complete;
      54             :   int       slot_complete;
      55             :   ulong     payload_sz;
      56             :   uchar *   payload;
      57             : } fd_shred_completed_fec_t;
      58             : 
      59             : #define SORT_NAME                sort_completed_fec
      60           0 : #define SORT_KEY_T               fd_shred_completed_fec_t
      61           0 : #define SORT_BEFORE(a,b)         ( (a).slot<(b).slot || ( (a).slot==(b).slot && (a).fec_set_idx<(b).fec_set_idx ) )
      62             : #define SORT_QUICK_SWAP_MINIMIZE 1 /* ~240B key; skip no-op swaps */
      63             : #include "../../../util/tmpl/fd_sort.c"
      64             : 
      65             : /* Return reasm's evicted single-child chain to the pool. */
      66             : static void
      67             : release_evicted_chain( fd_reasm_t *     reasm,
      68           0 :                        fd_reasm_fec_t * evicted ) {
      69           0 :   while( evicted ) {
      70           0 :     fd_reasm_fec_t * next = fd_reasm_child( reasm, evicted );
      71           0 :     fd_reasm_pool_release( reasm, evicted );
      72           0 :     evicted = next;
      73           0 :   }
      74           0 : }
      75             : 
      76             : /* This is a harness simplification that avoids having to use store.
      77             :    Also includes logic from the shred tile to concatenate FEC set
      78             :    payloads into a contiguous buffer. */
      79             : static void
      80             : capture_completed_fec( fd_spad_t *                spad,
      81             :                        fd_fec_set_t const *       set,
      82           0 :                        fd_shred_completed_fec_t * out ) {
      83           0 :   fd_memset( out, 0, sizeof(fd_shred_completed_fec_t) );
      84             : 
      85           0 :   fd_shred_t const * base_data   = fd_shred_parse( set->data_shreds  [ 0 ].b, FD_SHRED_MIN_SZ, FD_SHRED_BLK_MAX );
      86           0 :   fd_shred_t const * base_parity = fd_shred_parse( set->parity_shreds[ 0 ].b, FD_SHRED_MAX_SZ, FD_SHRED_BLK_MAX );
      87           0 :   FD_TEST( base_data && base_parity );
      88             : 
      89           0 :   ushort data_cnt = base_parity->code.data_cnt;
      90           0 :   ushort code_cnt = base_parity->code.code_cnt;
      91             : 
      92             :   /* Concatenate data-shred payloads into a single buffer. */
      93           0 :   uchar * payload    = fd_spad_alloc( spad, alignof(uchar), (ulong)data_cnt*FD_SHRED_MAX_SZ );
      94           0 :   ulong   payload_sz = 0UL;
      95           0 :   for( ushort i=0U; i<data_cnt; i++ ) {
      96           0 :     fd_shred_t const * shred = fd_shred_parse( set->data_shreds[ i ].b, FD_SHRED_MIN_SZ, FD_SHRED_BLK_MAX );
      97           0 :     FD_TEST( shred );
      98           0 :     ulong shred_payload_sz = fd_shred_payload_sz( shred );
      99           0 :     if( FD_LIKELY( shred_payload_sz ) ) {
     100           0 :       memcpy( payload + payload_sz, fd_shred_data_payload( shred ), shred_payload_sz );
     101           0 :       payload_sz += shred_payload_sz;
     102           0 :     }
     103           0 :     out->shred_offs[ i ] = (uint)payload_sz;
     104           0 :   }
     105             : 
     106           0 :   fd_shred_t const * last = fd_shred_parse( set->data_shreds[ data_cnt-1U ].b, FD_SHRED_MIN_SZ, FD_SHRED_BLK_MAX );
     107           0 :   FD_TEST( last );
     108             : 
     109             :   /* Derive the FEC set's merkle root from a shred's inclusion proof. */
     110           0 :   uchar bmtree_mem[ FD_BMTREE_COMMIT_FOOTPRINT( FD_SHRED_MERKLE_LAYER_CNT ) ] __attribute__((aligned(FD_BMTREE_COMMIT_ALIGN)));
     111           0 :   fd_bmtree_node_t root[1];
     112           0 :   FD_TEST( fd_shred_merkle_root( base_data, bmtree_mem, root ) );
     113           0 :   memcpy( out->mr.hash, root->hash, sizeof(out->mr.hash) );
     114           0 :   out->slot              = base_data->slot;
     115           0 :   out->fec_set_idx       = base_data->fec_set_idx;
     116           0 :   out->parent_off        = base_data->data.parent_off;
     117           0 :   out->num_data_shreds   = data_cnt;
     118           0 :   out->num_coding_shreds = code_cnt;
     119           0 :   out->shred_cnt         = data_cnt;
     120           0 :   out->data_complete     = !!(last->data.flags & FD_SHRED_DATA_FLAG_DATA_COMPLETE);
     121           0 :   out->slot_complete     = !!(last->data.flags & FD_SHRED_DATA_FLAG_SLOT_COMPLETE);
     122           0 :   out->payload_sz        = payload_sz;
     123           0 :   out->payload           = payload;
     124             : 
     125             :   /* Shreds are always chained, so the chained root is present. */
     126           0 :   memcpy( out->cmr.hash, (uchar const *)base_data + fd_shred_chain_off( base_data->variant ), FD_SHRED_MERKLE_ROOT_SZ );
     127           0 : }
     128             : 
     129             : ulong
     130             : fd_solfuzz_pb_shred_run( fd_solfuzz_runner_t * runner,
     131             :                          void const *          input_,
     132             :                          void **               output_,
     133             :                          void *                output_buf,
     134           0 :                          ulong                 output_bufsz ) {
     135           0 :   fd_exec_test_shred_parse_context_t const * input = fd_type_pun_const( input_ );
     136           0 :   fd_exec_test_shred_parse_effects_t **      output = fd_type_pun( output_ );
     137             : 
     138             :   /* Initialize output protobuf in the caller-provided buffer.  The
     139             :      fixed-size header allocations (effects struct, per-shred result
     140             :      array, per-FEC result array) must always fit; the caller is
     141             :      responsible for sizing output_buf, and a fit-check failure here
     142             :      is a harness wiring bug.  Variable-size payload writes inside the
     143             :      pop loop are checked separately and treated as soft failures. */
     144           0 :   ulong output_end = (ulong)output_buf + output_bufsz;
     145           0 :   FD_SCRATCH_ALLOC_INIT( l, output_buf );
     146             : 
     147           0 :   fd_exec_test_shred_parse_effects_t * effects =
     148           0 :     FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_exec_test_shred_parse_effects_t), sizeof(fd_exec_test_shred_parse_effects_t) );
     149           0 :   FD_TEST( _l <= output_end );
     150           0 :   fd_memset( effects, 0, sizeof(*effects) );
     151           0 :   effects->block_parse_result = FD_EXEC_TEST_BLOCK_PARSE_RESULT_ACCEPTED;
     152             : 
     153           0 :   ulong shred_cnt = (ulong)input->shreds_count;
     154           0 :   effects->shred_results_count = (pb_size_t)shred_cnt;
     155           0 :   effects->shred_results =
     156           0 :     FD_SCRATCH_ALLOC_APPEND( l, alignof(bool), sizeof(bool)*shred_cnt );
     157           0 :   FD_TEST( _l <= output_end );
     158           0 :   fd_memset( effects->shred_results, 0, sizeof(bool)*shred_cnt );
     159             : 
     160           0 :   ulong max_fec_results = shred_cnt;
     161           0 :   effects->fec_set_results =
     162           0 :     FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_exec_test_fec_set_parse_result_t), sizeof(fd_exec_test_fec_set_parse_result_t)*max_fec_results );
     163           0 :   FD_TEST( _l <= output_end );
     164           0 :   fd_memset( effects->fec_set_results, 0, sizeof(fd_exec_test_fec_set_parse_result_t)*max_fec_results );
     165           0 :   effects->fec_set_results_count = 0U;
     166             : 
     167             :   /* The resolver/sched bump FD_MCNT counters (e.g. reject paths) via a
     168             :      per-thread pointer that is NULL outside a tile.  Register a dummy
     169             :      write-sink to avoid a NULL deref; the values are never read. */
     170           0 :   static FD_TL uchar harness_metrics[ FD_METRICS_FOOTPRINT( 0 ) ] __attribute__((aligned(FD_METRICS_ALIGN)));
     171           0 :   fd_metrics_register( (ulong *)fd_metrics_new( harness_metrics, 0UL ) );
     172             : 
     173             :   /* Build a resolver configured for the fixture's shred version/root
     174             :      context. */
     175           0 :   ulong const resolver_set_cnt = RESOLVER_DEPTH + RESOLVER_PARTIAL_DEPTH + RESOLVER_COMPLETE_DEPTH;
     176             : 
     177           0 :   fd_fec_set_t * resolver_sets =
     178           0 :       fd_spad_alloc( runner->spad, alignof(fd_fec_set_t), sizeof(fd_fec_set_t)*resolver_set_cnt );
     179             : 
     180           0 :   ulong  resolver_footprint = fd_fec_resolver_footprint( RESOLVER_DEPTH,
     181           0 :                                                          RESOLVER_PARTIAL_DEPTH,
     182           0 :                                                          RESOLVER_COMPLETE_DEPTH,
     183           0 :                                                          RESOLVER_DONE_DEPTH );
     184           0 :   void * resolver_mem       = fd_spad_alloc( runner->spad, fd_fec_resolver_align(), resolver_footprint );
     185             : 
     186             :   /* Resolver/reasm/sched construction parameters are all compile-time
     187             :      constants or sourced from the trusted runner spad -- a NULL return
     188             :      from any of these constructors is a harness setup bug. */
     189           0 :   fd_fec_resolver_t * resolver = fd_fec_resolver_join( fd_fec_resolver_new(
     190           0 :       resolver_mem,
     191           0 :       NULL, NULL,
     192           0 :       RESOLVER_DEPTH,
     193           0 :       RESOLVER_PARTIAL_DEPTH,
     194           0 :       RESOLVER_COMPLETE_DEPTH,
     195           0 :       RESOLVER_DONE_DEPTH,
     196           0 :       resolver_sets,
     197           0 :       0UL ) );
     198           0 :   FD_TEST( resolver );
     199             : 
     200             :   /* Configure resolver behavior for the fuzz run:
     201             :        - bypass Merkle proof + Ed25519 signature checks
     202             :          (see fd_fec_resolver_set_bypass_verify in fd_fec_resolver.h)
     203             :        - drop shreds for slots strictly older than root_slot
     204             :        - map the proto bool for the discard-unexpected-DATA_COMPLETE
     205             :          feature onto the activation_slot extremes (0 = active for
     206             :          every slot, ULONG_MAX = never active) */
     207           0 :   fd_fec_resolver_set_shred_version( resolver, (ushort)input->shred_version );
     208           0 :   fd_fec_resolver_set_bypass_verify( resolver, 1 );
     209           0 :   fd_fec_resolver_advance_slot_old ( resolver, input->root_slot );
     210           0 :   fd_fec_resolver_set_discard_unexpected_data_complete_shreds( resolver,
     211           0 :       input->features.discard_unexpected_data_complete_shreds ? 0UL : ULONG_MAX );
     212             : 
     213             :   /* Initialize reasm/sched so completed FEC sets can be
     214             :      replay-ingested in order. */
     215           0 :   void * reasm_mem = fd_spad_alloc( runner->spad, fd_reasm_align(), fd_reasm_footprint( REASM_POOL_MAX ) );
     216           0 :   fd_reasm_t * reasm = fd_reasm_join( fd_reasm_new( reasm_mem, REASM_POOL_MAX, 0UL ) );
     217           0 :   FD_TEST( reasm );
     218           0 :   int reasm_initialized = 0;
     219             : 
     220           0 :   fd_rng_t rng_mem[1];
     221           0 :   fd_rng_t * rng = fd_rng_join( fd_rng_new( rng_mem, 0U, 0UL ) );
     222           0 :   void * sched_mem = fd_spad_alloc( runner->spad, fd_sched_align(), fd_sched_footprint( SCHED_DEPTH, SCHED_BLOCK_CNT_MAX ) );
     223           0 :   fd_sched_t * sched = fd_sched_join( fd_sched_new( sched_mem, rng, SCHED_DEPTH, SCHED_BLOCK_CNT_MAX, SCHED_EXEC_CNT ) );
     224           0 :   FD_TEST( sched );
     225           0 :   fd_sched_set_bypass_poh_verify( sched, 1 ); /* skip PoH end_hash compare */
     226           0 :   fd_sched_set_bypass_alut_resolution( sched, 1 ); /* skip ALUT resolution (no accdb) */
     227           0 :   fd_sched_block_add_done( sched, 0UL, ULONG_MAX, input->root_slot );
     228           0 :   fd_sched_root_notify( sched, 0UL );
     229           0 :   fd_sched_advance_root( sched, 0UL );
     230             : 
     231             :   /* completed[] caches FEC payloads for sched ingestion.  This is a
     232             :      simplified version of store specifically for the harness.  Instead
     233             :      of querying store for FEC set data, we just store it in this
     234             :      local array. */
     235           0 :   fd_shred_completed_fec_t * completed =
     236           0 :     fd_spad_alloc( runner->spad, alignof(fd_shred_completed_fec_t), sizeof(fd_shred_completed_fec_t)*max_fec_results );
     237           0 :   ulong completed_cnt = 0UL;
     238           0 :   ulong next_bank_idx = 1UL; /* 0 is the reasm/sched root, so start at 1 */
     239             : 
     240             :   /* Parse each shred, feed resolver, and only continue when an FEC
     241             :      completes. */
     242           0 :   fd_pubkey_t dummy_leader_pubkey = {0};
     243           0 :   for( ulong i=0UL; i<shred_cnt; i++ ) {
     244           0 :     pb_bytes_array_t const * shred_msg = input->shreds[ i ];
     245           0 :     FD_TEST( shred_msg );
     246           0 :     FD_TEST( shred_msg->size>=FD_SHRED_MIN_SZ && shred_msg->size<=FD_SHRED_MAX_SZ );
     247             : 
     248             :     /* Step 1: fd_shred_parse() */
     249           0 :     fd_shred_t const * shred = fd_shred_parse( shred_msg->bytes, shred_msg->size, FD_SHRED_BLK_MAX );
     250             : 
     251             :     /* Unchained merkle shreds are dropped in the shred tile. */
     252           0 :     if( !shred || !fd_shred_is_chained( fd_shred_type( shred->variant ) ) ) {
     253           0 :       effects->shred_results[ i ] = false;
     254           0 :       continue;
     255           0 :     }
     256             : 
     257             :     /* Parse-level accept, matching Agave; the resolver verdict below is not folded in. */
     258           0 :     effects->shred_results[ i ] = true;
     259             : 
     260             :     /* Step 2: fd_fec_resolver_add_shred() */
     261           0 :     fd_fec_set_t const * out_fec_set = NULL;
     262           0 :     fd_shred_t const *   out_shred   = NULL;
     263           0 :     fd_bmtree_node_t     out_merkle_root[1];
     264           0 :     int rc = fd_fec_resolver_add_shred(
     265           0 :       resolver,
     266           0 :       shred,
     267           0 :       shred_msg->size,
     268           0 :       FD_SHRED_BLK_MAX,
     269           0 :       0,
     270           0 :       dummy_leader_pubkey.uc,
     271           0 :       &out_fec_set,
     272           0 :       &out_shred,
     273           0 :       out_merkle_root,
     274           0 :       NULL
     275           0 :     );
     276           0 :     (void)out_shred;
     277             : 
     278             :     /* We only want to replay the FEC set once it's complete. */
     279           0 :     if( rc!=FD_FEC_RESOLVER_SHRED_COMPLETES ) continue;
     280             : 
     281             :     /* completed[] is sized to shred_cnt, which is a strict upper bound
     282             :        on completion count, so overflow here is a harness sizing bug. */
     283           0 :     FD_TEST( completed_cnt<max_fec_results );
     284             : 
     285             :     /* Construct the completed FEC set record.  The merkle root is derived
     286             :        from the shred's inclusion proof inside capture_completed_fec. */
     287           0 :     fd_shred_completed_fec_t * rec = &completed[ completed_cnt ];
     288           0 :     capture_completed_fec( runner->spad, out_fec_set, rec );
     289             : 
     290             :     /* Buffer the completion; reasm runs after the shred loop in
     291             :        (slot, fec_set_idx) order. */
     292           0 :     completed_cnt++;
     293           0 :   }
     294             : 
     295             :   /* Sort by (slot, fec_set_idx) so set 0 anchors the reasm root
     296             :      before any successor inserts. */
     297           0 :   sort_completed_fec_inplace( completed, completed_cnt );
     298             : 
     299           0 :   for( ulong k=0UL; k<completed_cnt; k++ ) {
     300           0 :     fd_shred_completed_fec_t * rec = &completed[ k ];
     301             : 
     302             :     /* Lazily init reasm off the first (lowest slot/fec_set_idx) FEC to set the root block id. */
     303           0 :     if( FD_UNLIKELY( !reasm_initialized ) ) {
     304           0 :       fd_reasm_fec_t * root = fd_reasm_init( reasm, &rec->cmr, input->root_slot );
     305           0 :       if( FD_UNLIKELY( !root ) ) {
     306           0 :         effects->block_parse_result = FD_EXEC_TEST_BLOCK_PARSE_RESULT_REJECTED_INVALID_HEADER;
     307           0 :         continue;
     308           0 :       }
     309           0 :       root->bank_idx    = 0UL;
     310           0 :       reasm_initialized = 1;
     311           0 :     }
     312             : 
     313             :     /* Step 3: fd_reasm_insert()
     314             :        Insert completed FEC into reasm and release elements from evicted
     315             :        chains. */
     316           0 :     fd_reasm_fec_t * evicted = NULL;
     317           0 :     if( !fd_reasm_insert( reasm,
     318           0 :                           &rec->mr,
     319           0 :                           &rec->cmr,
     320           0 :                           rec->slot,
     321           0 :                           rec->fec_set_idx,
     322           0 :                           rec->parent_off,
     323           0 :                           rec->num_data_shreds,
     324           0 :                           rec->data_complete,
     325           0 :                           rec->slot_complete,
     326           0 :                           0,
     327           0 :                           NULL,
     328           0 :                           &evicted ) ) {
     329           0 :       release_evicted_chain( reasm, evicted );
     330           0 :       effects->block_parse_result = FD_EXEC_TEST_BLOCK_PARSE_RESULT_REJECTED_INVALID_HEADER;
     331           0 :       continue;
     332           0 :     }
     333           0 :     release_evicted_chain( reasm, evicted );
     334             : 
     335             :     /* Step 4: fd_reasm_pop() and fd_fec_set_ingest() while there are
     336             :        completed FEC sets remaining. */
     337           0 :     fd_reasm_fec_t * popped;
     338           0 :     while( FD_LIKELY( (popped = fd_reasm_pop( reasm )) ) ) {
     339             :       /* Query our "store"-adjacent structure (completed) for the FEC
     340             :          set payload */
     341           0 :       fd_shred_completed_fec_t * popped_rec = NULL;
     342           0 :       for( ulong j=0UL; j<completed_cnt; j++ ) {
     343           0 :         if( FD_LIKELY( !memcmp( completed[ j ].mr.hash, popped->key.hash, sizeof(fd_hash_t) ) ) ) {
     344           0 :           popped_rec = &completed[ j ];
     345           0 :           break;
     346           0 :         }
     347           0 :       }
     348           0 :       FD_TEST( popped_rec );
     349           0 :       FD_TEST( effects->fec_set_results_count<max_fec_results );
     350             : 
     351             :       /* Capture completed FEC set results */
     352           0 :       fd_exec_test_fec_set_parse_result_t * out_fec = &effects->fec_set_results[ effects->fec_set_results_count++ ];
     353           0 :       fd_memset( out_fec, 0, sizeof(*out_fec) );
     354           0 :       out_fec->completed         = true;
     355           0 :       out_fec->slot              = popped_rec->slot;
     356           0 :       out_fec->fec_set_index     = popped_rec->fec_set_idx;
     357           0 :       out_fec->parent_offset     = popped_rec->parent_off;
     358           0 :       out_fec->shred_version     = input->shred_version;
     359           0 :       out_fec->num_data_shreds   = popped_rec->num_data_shreds;
     360           0 :       out_fec->num_coding_shreds = popped_rec->num_coding_shreds;
     361           0 :       memcpy( out_fec->merkle_root, popped_rec->mr.hash, FD_SHRED_MERKLE_ROOT_SZ );
     362           0 :       memcpy( out_fec->chained_merkle_root, popped_rec->cmr.hash, FD_SHRED_MERKLE_ROOT_SZ );
     363           0 :       if( FD_LIKELY( popped_rec->payload_sz ) ) {
     364           0 :         out_fec->payload = FD_SCRATCH_ALLOC_APPEND( l, alignof(pb_bytes_array_t), PB_BYTES_ARRAY_T_ALLOCSIZE( popped_rec->payload_sz ) );
     365           0 :         if( FD_UNLIKELY( _l > output_end ) ) {
     366           0 :           effects->block_parse_result = FD_EXEC_TEST_BLOCK_PARSE_RESULT_REJECTED_INVALID_HEADER;
     367           0 :           effects->fec_set_results_count--;
     368           0 :           break;
     369           0 :         }
     370           0 :         out_fec->payload->size = (pb_size_t)popped_rec->payload_sz;
     371           0 :         memcpy( out_fec->payload->bytes, popped_rec->payload, popped_rec->payload_sz );
     372           0 :       }
     373             : 
     374             :       /* Match Agave's model of only parsing deshreddable batches.
     375             :          Otherwise, Firedancer's eager per-FEC parsing might reject a
     376             :          block for a bad FEC set that Agave simply doesn't parse at all.
     377             :          A FEC set is deshreddable iff some FEC set at or after it in
     378             :          the same slot carries DATA_COMPLETE.  Complete batches are
     379             :          still fed one FEC set at a time.  Only the final, potentially
     380             :          incomplete batch is held back.  fec_set_results were already
     381             :          captured above, independent of the scheduler, so withholding
     382             :          does not change them.  Agave likewise emits a fec_set_result
     383             :          for every complete FEC set, deshreddable or not. */
     384           0 :       int deshreddable = 0;
     385           0 :       for( ulong j=0UL; j<completed_cnt; j++ ) {
     386           0 :         if( completed[ j ].slot==popped_rec->slot && completed[ j ].fec_set_idx>=popped_rec->fec_set_idx && completed[ j ].data_complete ) {
     387           0 :           deshreddable = 1;
     388           0 :           break;
     389           0 :         }
     390           0 :       }
     391           0 :       if( !deshreddable ) continue;
     392             : 
     393             :       /* Bank lineage comes from the reasm tree, like the replay tile.
     394             :          Reasm pops parents before children, so the parent's bank_idx
     395             :          was already recorded by the time we read it here. */
     396           0 :       fd_reasm_fec_t * parent = fd_reasm_parent( reasm, popped );
     397           0 :       FD_TEST( parent );
     398           0 :       ulong parent_bank_idx = parent->bank_idx;
     399             : 
     400             :       /* If the FEC set starts a new slot (no bank yet), use a fresh
     401             :          bank index; otherwise reuse the parent's.  Record it on the
     402             :          node so this slot's later FECs inherit it. */
     403           0 :       ulong bank_idx = ( popped->fec_set_idx==0U ) ? next_bank_idx++ : parent_bank_idx;
     404           0 :       popped->bank_idx = bank_idx;
     405             : 
     406           0 :       fd_store_fec_t store_fec[1] = {0};
     407           0 :       store_fec->key.merkle_root = popped_rec->mr;
     408           0 :       store_fec->data_sz         = popped_rec->payload_sz;
     409           0 :       memcpy( store_fec->shred_offs, popped_rec->shred_offs, sizeof(store_fec->shred_offs) );
     410             : 
     411           0 :       fd_sched_fec_t sched_fec = {
     412           0 :         .bank_idx          = bank_idx,
     413           0 :         .parent_bank_idx   = parent_bank_idx,
     414           0 :         .slot              = popped_rec->slot,
     415           0 :         .parent_slot       = fd_ulong_if( popped_rec->slot>=popped_rec->parent_off, popped_rec->slot-popped_rec->parent_off, 0UL ),
     416           0 :         .fec               = store_fec,
     417           0 :         .data              = popped_rec->payload,
     418           0 :         .shred_cnt         = popped_rec->shred_cnt,
     419           0 :         .is_last_in_batch  = !!popped_rec->data_complete,
     420           0 :         .is_last_in_block  = !!popped_rec->slot_complete,
     421           0 :         .is_first_in_block = !!(popped_rec->fec_set_idx==0U)
     422           0 :       };
     423             : 
     424             :       /* Drain abandoned blocks; fd_sched_fec_ingest requires an empty
     425             :          ref_q.  The replay tile drains the same queue (to decrement bank
     426             :          refcounts); the harness has no banks, so it just discards. */
     427           0 :       while( fd_sched_pruned_block_next( sched )!=ULONG_MAX ) {}
     428             : 
     429             :       /* Final step: ingest the completed FEC set. */
     430           0 :       FD_TEST( fd_sched_fec_can_ingest( sched, &sched_fec ) );
     431           0 :       if( !fd_sched_fec_ingest( sched, &sched_fec ) ) {
     432           0 :         effects->block_parse_result = FD_EXEC_TEST_BLOCK_PARSE_RESULT_REJECTED_INVALID_HEADER;
     433           0 :       } else if( FD_LIKELY( bank_idx!=0UL ) ) {
     434             :         /* Harness stops at FEC ingest, so verify this slot's tick
     435             :            window here; skip bank_idx 0 (reasm/sched root).  Only
     436             :            complete batches reach this point, so this matches Agave's
     437             :            model of only verifying deshredded batches. */
     438           0 :         if( fd_sched_block_verify_ticks( sched, bank_idx, 0UL, SLOT_MAX_TICK_HEIGHT, SLOT_HASHES_PER_TICK ) ) {
     439           0 :           effects->block_parse_result = FD_EXEC_TEST_BLOCK_PARSE_RESULT_REJECTED_INVALID_HEADER;
     440           0 :         }
     441           0 :       }
     442           0 :     }
     443           0 :   }
     444             : 
     445             :   /* Finalize scratch allocation and return encoded effects span. */
     446           0 :   ulong actual_end = FD_SCRATCH_ALLOC_FINI( l, 1UL );
     447           0 :   *output = effects;
     448           0 :   return actual_end - (ulong)output_buf;
     449           0 : }
     450             : 

Generated by: LCOV version 1.14