LCOV - code coverage report
Current view: top level - app/firedancer-dev/commands - force_mismatch.c (source / functions) Hit Total Coverage
Test: cov.lcov Lines: 0 146 0.0 %
Date: 2026-11-11 12:40:43 Functions: 0 9 0.0 %

          Line data    Source code
       1             : /* force-mismatch attaches to a running validator (or backtest) and
       2             :    forces a genuine bank hash mismatch at a chosen slot, without any
       3             :    support from the validator itself.
       4             : 
       5             :    It works by injecting a hard fork into the bank state in shared
       6             :    memory.  A hard fork at slot T is mixed into the bank hash of the
       7             :    first block with parent_slot<T<=slot, and also moves the
       8             :    LastRestartSlot sysvar to T, so the validator computes a bank hash
       9             :    the rest of the cluster (or the ledger) disagrees with.  This is the
      10             :    same mechanism an operator's --hard-fork uses, so every downstream
      11             :    mismatch path (backtest ledger comparison, Alpenglow footer check,
      12             :    tower hard fork detection) is exercised for real.
      13             : 
      14             :    Hard forks are carried in bank->f and copied from parent to child
      15             :    when a bank becomes replayable, so the fork is appended to every live
      16             :    bank with slot<T.  Descendants inherit it from there.  The shred
      17             :    version is computed from the replay tile's private copy of the
      18             :    snapshot hard forks, so it is unaffected. */
      19             : 
      20             : #include "../../shared/fd_config.h"
      21             : #include "../../shared/fd_action.h"
      22             : #include "../../shared/fd_bootinfo.h"
      23             : #include "../../../flamenco/runtime/fd_bank.h"
      24             : #include "../../../util/pod/fd_pod.h"
      25             : #include "../../../util/wksp/fd_wksp_private.h"
      26             : 
      27             : #include <errno.h>
      28             : #include <fcntl.h>
      29             : #include <unistd.h>
      30             : #include <sys/stat.h>
      31             : 
      32             : extern action_t * ACTIONS[];
      33             : 
      34             : static void
      35             : force_mismatch_cmd_args( int *    pargc,
      36             :                          char *** pargv,
      37           0 :                          args_t * args ) {
      38           0 :   char const * topo_name = fd_env_strip_cmdline_cstr( pargc, pargv, "--topo", NULL, "" );
      39             : 
      40           0 :   ulong topo_name_len = strlen( topo_name );
      41           0 :   if( FD_UNLIKELY( topo_name_len > sizeof(args->force_mismatch.topo)-1 ) ) FD_LOG_ERR(( "Unknown --topo %s", topo_name ));
      42           0 :   fd_cstr_fini( fd_cstr_append_text( fd_cstr_init( args->force_mismatch.topo ), topo_name, topo_name_len ) );
      43             : 
      44           0 :   args->force_mismatch.at_slot = fd_env_strip_cmdline_ulong( pargc, pargv, "--at-slot", NULL, 0UL );
      45           0 :   args->force_mismatch.after   = fd_env_strip_cmdline_ulong( pargc, pargv, "--after",   NULL, 0UL );
      46             : 
      47           0 :   if( FD_UNLIKELY( !args->force_mismatch.at_slot == !args->force_mismatch.after ) ) {
      48           0 :     FD_LOG_ERR(( "exactly one of --at-slot <slot> or --after <slots> must be given" ));
      49           0 :   }
      50           0 : }
      51             : 
      52             : static void
      53             : reconstruct_topo( config_t *   config,
      54           0 :                   char const * topo_name ) {
      55           0 :   if( !topo_name[0] ) return; /* keep default action topo */
      56             : 
      57           0 :   action_t const * selected = NULL;
      58           0 :   for( action_t ** a=ACTIONS; *a; a++ ) {
      59           0 :     action_t const * action = *a;
      60           0 :     if( 0==strcmp( action->name, topo_name ) ) {
      61           0 :       selected = action;
      62           0 :       break;
      63           0 :     }
      64           0 :   }
      65             : 
      66           0 :   if( !selected       ) FD_LOG_ERR(( "Unknown --topo %s", topo_name ));
      67           0 :   if( !selected->topo ) FD_LOG_ERR(( "Cannot recover topology for --topo %s", topo_name ));
      68             : 
      69           0 :   selected->topo( config );
      70           0 : }
      71             : 
      72             : static int
      73             : bank_has_fork( fd_bank_t const * bank,
      74           0 :                ulong             slot ) {
      75           0 :   ulong cnt = fd_ulong_min( FD_VOLATILE_CONST( bank->f.hard_fork_cnt ), FD_HARD_FORKS_MAX );
      76           0 :   for( ulong i=0UL; i<cnt; i++ ) {
      77           0 :     if( FD_VOLATILE_CONST( bank->f.hard_forks[ i ].slot )==slot ) return 1;
      78           0 :   }
      79           0 :   return 0;
      80           0 : }
      81             : 
      82             : /* bank_add_fork appends the hard fork before publishing the new count,
      83             :    so a concurrent reader sees either the old or the new list. */
      84             : 
      85             : static void
      86             : bank_add_fork( fd_bank_t * bank,
      87           0 :                ulong       slot ) {
      88           0 :   ulong cnt = FD_VOLATILE_CONST( bank->f.hard_fork_cnt );
      89           0 :   if( FD_UNLIKELY( cnt>=FD_HARD_FORKS_MAX ) ) FD_LOG_ERR(( "bank for slot %lu already has %lu hard forks", bank->f.slot, cnt ));
      90           0 :   bank->f.hard_forks[ cnt ].slot = slot;
      91           0 :   bank->f.hard_forks[ cnt ].cnt  = 1UL;
      92           0 :   FD_COMPILER_MFENCE();
      93           0 :   FD_VOLATILE( bank->f.hard_fork_cnt ) = cnt+1UL;
      94           0 : }
      95             : 
      96             : /* max_replayed_slot returns the highest slot among replayable and
      97             :    frozen banks, or 0 if there are none yet. */
      98             : 
      99             : static ulong
     100             : max_replayed_slot( fd_bank_t const * pool,
     101           0 :                    ulong             bank_max ) {
     102           0 :   ulong max = 0UL;
     103           0 :   for( ulong i=0UL; i<bank_max; i++ ) {
     104           0 :     ulong state = FD_VOLATILE_CONST( pool[ i ].state );
     105           0 :     if( state!=FD_BANK_STATE_REPLAYABLE && state!=FD_BANK_STATE_FROZEN ) continue;
     106           0 :     max = fd_ulong_max( max, FD_VOLATILE_CONST( pool[ i ].f.slot ) );
     107           0 :   }
     108           0 :   return max;
     109           0 : }
     110             : 
     111             : static int
     112           0 : any_live_validator( void ) {
     113           0 :   fd_bootinfo_instance_t instances[ FD_BOOTINFO_INSTANCE_MAX ];
     114           0 :   ulong cnt = fd_bootinfo_discover( instances, FD_BOOTINFO_INSTANCE_MAX );
     115           0 :   for( ulong i=0UL; i<cnt; i++ ) if( instances[ i ].live ) return 1;
     116           0 :   return 0;
     117           0 : }
     118             : 
     119             : /* wksp_ready returns 1 if the workspace file exists, was created no
     120             :    earlier than min_ctime_nanos, and has been formatted. */
     121             : 
     122             : static int
     123             : wksp_ready( config_t const *       config,
     124             :             fd_topo_wksp_t const * wksp,
     125           0 :             long                   min_ctime_nanos ) {
     126           0 :   char const * mount_path;
     127           0 :   switch( wksp->page_sz ) {
     128           0 :     case FD_SHMEM_GIGANTIC_PAGE_SZ: mount_path = config->hugetlbfs.gigantic_page_mount_path; break;
     129           0 :     case FD_SHMEM_HUGE_PAGE_SZ:     mount_path = config->hugetlbfs.huge_page_mount_path;     break;
     130           0 :     default:                        mount_path = config->hugetlbfs.normal_page_mount_path;   break;
     131           0 :   }
     132           0 :   char path[ PATH_MAX ];
     133           0 :   FD_TEST( fd_cstr_printf_check( path, PATH_MAX, NULL, "%s/%s_%s.wksp", mount_path, config->name, wksp->name ) );
     134             : 
     135           0 :   int fd = open( path, O_RDONLY );
     136           0 :   if( FD_UNLIKELY( -1==fd ) ) return 0;
     137           0 :   struct stat st;
     138           0 :   ulong magic = 0UL;
     139           0 :   int ready = !fstat( fd, &st ) &&
     140           0 :               (long)st.st_ctim.tv_sec*(long)1e9 + st.st_ctim.tv_nsec>=min_ctime_nanos &&
     141           0 :               pread( fd, &magic, sizeof(ulong), 0L )==(long)sizeof(ulong) &&
     142           0 :               magic==FD_WKSP_MAGIC;
     143           0 :   if( FD_UNLIKELY( -1==close( fd ) ) ) FD_LOG_ERR(( "close(%s) failed (%i-%s)", path, errno, fd_io_strerror( errno ) ));
     144           0 :   return ready;
     145           0 : }
     146             : 
     147             : static void
     148             : force_mismatch_cmd_fn( args_t *   args,
     149           0 :                        config_t * config ) {
     150           0 :   long start_nanos = fd_log_wallclock();
     151             : 
     152             :   /* A short backtest can replay past the target within milliseconds of
     153             :      booting, so allow starting this before the validator. */
     154           0 :   if( FD_LIKELY( !config->has_user_config ) ) {
     155           0 :     FD_LOG_NOTICE(( "waiting for a running validator" ));
     156           0 :     while( !any_live_validator() ) fd_log_sleep( (long)1e6 );
     157           0 :   }
     158             : 
     159           0 :   fd_bootinfo_adopt( config );
     160           0 :   reconstruct_topo( config, args->force_mismatch.topo );
     161           0 :   fd_bootinfo_check_layout( config );
     162             : 
     163           0 :   fd_topo_t * topo = &config->topo;
     164           0 :   ulong banks_obj_id = fd_pod_query_ulong( topo->props, "banks", ULONG_MAX );
     165           0 :   if( FD_UNLIKELY( banks_obj_id==ULONG_MAX ) ) FD_LOG_ERR(( "banks object not found in topology" ));
     166           0 :   fd_topo_wksp_t * wksp = &topo->workspaces[ topo->objs[ banks_obj_id ].wksp_id ];
     167             : 
     168             :   /* A backtest publishes no bootinfo and leaves its workspaces behind
     169             :      on exit, so unless a live validator was found, only attach to a
     170             :      workspace created after this command started. */
     171           0 :   long min_ctime_nanos = config->boot_timestamp_nanos ? 0L : start_nanos;
     172           0 :   if( min_ctime_nanos ) FD_LOG_NOTICE(( "waiting for %s_%s.wksp to be created", topo->app_name, wksp->name ));
     173           0 :   while( !wksp_ready( config, wksp, min_ctime_nanos ) ) fd_log_sleep( (long)1e6 );
     174           0 :   fd_topo_join_workspace( topo, wksp, FD_SHMEM_JOIN_MODE_READ_WRITE, FD_TOPO_CORE_DUMP_LEVEL_DISABLED );
     175             : 
     176             :   /* Banks are accessed raw rather than with fd_banks_join, which would
     177             :      also attach the file backed per-fork stores. */
     178           0 :   fd_banks_t * banks = fd_topo_obj_laddr( topo, banks_obj_id );
     179           0 :   while( FD_VOLATILE_CONST( banks->magic )!=FD_BANKS_MAGIC ) fd_log_sleep( (long)1e6 );
     180           0 :   fd_bank_t * pool     = fd_type_pun( (uchar *)banks + banks->pool_offset );
     181           0 :   ulong       bank_max = banks->max_total_banks;
     182             : 
     183             :   /* Wait for the snapshot to load so there is a replayed slot to count
     184             :      from, and to refuse a target that has already been replayed. */
     185           0 :   ulong replayed;
     186           0 :   while( !(replayed=max_replayed_slot( pool, bank_max )) ) FD_SPIN_PAUSE();
     187             : 
     188           0 :   ulong target = args->force_mismatch.at_slot;
     189           0 :   if( args->force_mismatch.after ) target = replayed + args->force_mismatch.after;
     190           0 :   if( FD_UNLIKELY( target<=replayed ) ) {
     191           0 :     FD_LOG_ERR(( "--at-slot %lu has already been replayed (highest replayed slot %lu)", target, replayed ));
     192           0 :   }
     193           0 :   FD_LOG_NOTICE(( "highest replayed slot %lu, injecting hard fork at slot %lu", replayed, target ));
     194             : 
     195             :   /* Keep every bank below the target carrying the fork until a block
     196             :      crossing the target is replayed.  Rescanning repairs banks that
     197             :      were cloned from a parent before it was patched (or rewritten by
     198             :      the snapshot loader). */
     199           0 :   ulong patched = 0UL;
     200           0 :   for(;;) {
     201           0 :     for( ulong i=0UL; i<bank_max; i++ ) {
     202           0 :       fd_bank_t * bank  = &pool[ i ];
     203           0 :       ulong       state = FD_VOLATILE_CONST( bank->state );
     204           0 :       if( state!=FD_BANK_STATE_REPLAYABLE && state!=FD_BANK_STATE_FROZEN && state!=FD_BANK_STATE_PRUNABLE ) continue;
     205             : 
     206           0 :       ulong slot = FD_VOLATILE_CONST( bank->f.slot );
     207           0 :       if( slot<target ) {
     208           0 :         if( !bank_has_fork( bank, target ) ) { bank_add_fork( bank, target ); patched++; }
     209           0 :         continue;
     210           0 :       }
     211             : 
     212             :       /* A prunable bank may never have been replayable, and banks past
     213             :          the first crossing are judged by their ancestor. */
     214           0 :       ulong parent_slot = FD_VOLATILE_CONST( bank->f.parent_slot );
     215           0 :       if( state==FD_BANK_STATE_PRUNABLE || parent_slot>=target ) continue;
     216             : 
     217           0 :       if( FD_UNLIKELY( !bank_has_fork( bank, target ) ) ) {
     218           0 :         if( state==FD_BANK_STATE_FROZEN ) {
     219           0 :           FD_LOG_ERR(( "slot %lu (parent %lu) froze before the hard fork could be injected", slot, parent_slot ));
     220           0 :         }
     221             :         /* Cloned from an unpatched parent but still executing.  The
     222             :            bank hash still picks up the fork when the bank freezes. */
     223           0 :         FD_LOG_WARNING(( "slot %lu (parent %lu) is already replaying, injecting late", slot, parent_slot ));
     224           0 :         bank_add_fork( bank, target );
     225           0 :         patched++;
     226           0 :       }
     227             : 
     228           0 :       ulong bank_seq = FD_VOLATILE_CONST( bank->bank_seq );
     229           0 :       FD_LOG_NOTICE(( "slot %lu (parent %lu) replays with the hard fork (%lu banks patched), waiting for it to complete", slot, parent_slot, patched ));
     230           0 :       while( FD_VOLATILE_CONST( bank->state )==FD_BANK_STATE_REPLAYABLE && FD_VOLATILE_CONST( bank->bank_seq )==bank_seq ) FD_SPIN_PAUSE();
     231           0 :       state = FD_VOLATILE_CONST( bank->state );
     232           0 :       if( FD_UNLIKELY( FD_VOLATILE_CONST( bank->bank_seq )!=bank_seq || (state!=FD_BANK_STATE_FROZEN && state!=FD_BANK_STATE_DEAD) ) ) {
     233           0 :         FD_LOG_WARNING(( "slot %lu was pruned before completing", slot ));
     234           0 :         return;
     235           0 :       }
     236             :       /* Under Alpenglow the footer check marks the mismatched block
     237             :          dead, otherwise it freezes and the mismatch surfaces later
     238             :          (backtest comparison, tower hard fork detection). */
     239           0 :       FD_BASE58_ENCODE_32_BYTES( bank->f.bank_hash.uc, bank_hash_b58 );
     240           0 :       FD_LOG_NOTICE(( "slot %lu %s with forced bank hash %s", slot, state==FD_BANK_STATE_DEAD ? "was marked dead" : "froze", bank_hash_b58 ));
     241           0 :       return;
     242           0 :     }
     243           0 :     FD_SPIN_PAUSE();
     244           0 :   }
     245           0 : }
     246             : 
     247             : static void
     248           0 : force_mismatch_args_help( fd_action_help_t * help ) {
     249           0 :   fd_action_help_arg( help, "--at-slot", "<slot>",    "Force the mismatch on the first block at or after this absolute slot" );
     250           0 :   fd_action_help_arg( help, "--after",   "<slots>",   "Force the mismatch this many slots past the highest replayed slot" );
     251           0 :   fd_action_help_arg( help, "--topo",    "<command>", "Build the topology from another subcommand (e.g. `backtest`) instead of\n"
     252           0 :                                                       "the default validator topology" );
     253           0 : }
     254             : 
     255             : action_t fd_action_force_mismatch = {
     256             :   .name          = "force-mismatch",
     257             :   .args          = force_mismatch_cmd_args,
     258             :   .fn            = force_mismatch_cmd_fn,
     259             :   .perm          = NULL,
     260             :   .description   = "Force a bank hash mismatch in a running validator",
     261             :   .detail        = "Attaches to a running validator or backtest and injects a hard fork into\n"
     262             :                    "its bank state, so the first block at or after the target slot computes\n"
     263             :                    "a bank hash that disagrees with the cluster or ledger.  Exactly one of\n"
     264             :                    "--at-slot or --after must be given.",
     265             :   .usage         = "force-mismatch (--at-slot <slot> | --after <slots>) [--topo <command>]",
     266             :   .args_help     = force_mismatch_args_help,
     267             :   .is_immediate  = 0,
     268             : };

Generated by: LCOV version 1.14