Skip to content

Commit c952b77

Browse files
committed
stake_ci: add APIs for updating or removing individual contact info entries
tests
1 parent 0b3b2b3 commit c952b77

File tree

3 files changed

+319
-1
lines changed

3 files changed

+319
-1
lines changed

src/disco/shred/fd_stake_ci.c

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "fd_stake_ci.h"
2+
#include "fd_shred_dest.h"
23
#include "../../util/net/fd_ip4.h" /* Just for debug */
34

45
#define SORT_NAME sort_pubkey
@@ -390,6 +391,100 @@ fd_stake_ci_set_identity( fd_stake_ci_t * info,
390391
*info->identity_key = *identity_key;
391392
}
392393

394+
void
395+
refresh_sdest( fd_stake_ci_t * info,
396+
fd_shred_dest_weighted_t * shred_dest_temp,
397+
ulong cnt,
398+
ulong staked_cnt,
399+
fd_per_epoch_info_t * ei ) {
400+
sort_pubkey_inplace( shred_dest_temp + staked_cnt, cnt - staked_cnt );
401+
402+
fd_shred_dest_delete( fd_shred_dest_leave( ei->sdest ) );
403+
ei->sdest = fd_shred_dest_join( fd_shred_dest_new( ei->_sdest, shred_dest_temp, cnt, ei->lsched, info->identity_key, ei->excluded_stake ) );
404+
if( FD_UNLIKELY( ei->sdest==NULL ) ) {
405+
FD_LOG_ERR(( "Too many validators have higher stake than this validator. Cannot continue." ));
406+
}
407+
}
408+
409+
void
410+
ci_dest_add_one_unstaked( fd_stake_ci_t * info,
411+
fd_shred_dest_weighted_t * new_entry,
412+
fd_per_epoch_info_t * ei ) {
413+
if( fd_shred_dest_cnt_all( ei->sdest )>=MAX_SHRED_DESTS ) {
414+
FD_LOG_WARNING(( "Too many validators in shred table to add a new validator." ));
415+
}
416+
ulong cur_cnt = fd_shred_dest_cnt_all( ei->sdest );
417+
for( ulong i=0UL; i<cur_cnt; i++ ) {
418+
info->shred_dest_temp[ i ] = *fd_shred_dest_idx_to_dest( ei->sdest, (fd_shred_dest_idx_t)i );
419+
}
420+
421+
/* TODO: Alternative batched copy using memcpy. Check with Philip if safe */
422+
// fd_shred_dest_weighted_t * cur_dest = ei->sdest->all_destinations;
423+
// fd_memcpy( info->shred_dest_temp, cur_dest, sizeof(fd_shred_dest_weighted_t)*cur_cnt );
424+
info->shred_dest_temp[ cur_cnt++ ] = *new_entry;
425+
refresh_sdest( info, info->shred_dest_temp, cur_cnt, fd_shred_dest_cnt_staked( ei->sdest ), ei );
426+
}
427+
428+
void
429+
ci_dest_update_impl( fd_stake_ci_t * info,
430+
fd_pubkey_t const * pubkey,
431+
uint ip4,
432+
ushort port,
433+
fd_per_epoch_info_t * ei ) {
434+
fd_shred_dest_idx_t idx = fd_shred_dest_pubkey_to_idx( ei->sdest, pubkey );
435+
if( idx==FD_SHRED_DEST_NO_DEST ) {
436+
fd_shred_dest_weighted_t new_entry = { .pubkey = *pubkey, .ip4 = ip4, .port = port, .stake_lamports = 0UL };
437+
ci_dest_add_one_unstaked( info, &new_entry, ei );
438+
return;
439+
}
440+
fd_shred_dest_weighted_t * dest = fd_shred_dest_idx_to_dest( ei->sdest, idx );
441+
dest->ip4 = ip4;
442+
dest->port = port;
443+
}
444+
445+
void
446+
ci_dest_remove_impl( fd_stake_ci_t * info,
447+
fd_pubkey_t const * pubkey,
448+
fd_per_epoch_info_t * ei ) {
449+
fd_shred_dest_idx_t idx = fd_shred_dest_pubkey_to_idx( ei->sdest, pubkey );
450+
if( FD_UNLIKELY( idx==FD_SHRED_DEST_NO_DEST ) ) return;
451+
452+
fd_shred_dest_weighted_t * dest = fd_shred_dest_idx_to_dest( ei->sdest, idx );
453+
if( FD_UNLIKELY( dest->stake_lamports>0UL ) ) {
454+
/* A staked entry is not "removed", instead its "stale" address is
455+
retained */
456+
return;
457+
}
458+
459+
ulong cur_cnt = fd_shred_dest_cnt_all( ei->sdest );
460+
for( ulong i=0UL, j=0UL; i<cur_cnt; i++ ) {
461+
if( FD_UNLIKELY( i==idx ) ) continue;
462+
info->shred_dest_temp[ j++ ] = *fd_shred_dest_idx_to_dest( ei->sdest, (fd_shred_dest_idx_t) i );
463+
}
464+
/* TODO: Alternative batched copy using memcpy. Check with Philip if this is safe */
465+
// fd_shred_dest_weighted_t * cur_dest = ei->sdest->all_destinations;
466+
// fd_memcpy( info->shred_dest_temp, cur_dest, sizeof(fd_shred_dest_weighted_t)*(idx) );
467+
// fd_memcpy( info->shred_dest_temp + idx, cur_dest + idx + 1UL, sizeof(fd_shred_dest_weighted_t)*(cur_cnt - idx - 1UL) );
468+
refresh_sdest( info, info->shred_dest_temp, cur_cnt-1UL, fd_shred_dest_cnt_staked( ei->sdest ), ei );
469+
}
470+
471+
void
472+
fd_stake_ci_dest_update( fd_stake_ci_t * info,
473+
fd_pubkey_t const * pubkey,
474+
uint ip4,
475+
ushort port ) {
476+
ci_dest_update_impl( info, pubkey, ip4, port, info->epoch_info+0UL );
477+
ci_dest_update_impl( info, pubkey, ip4, port, info->epoch_info+1UL );
478+
}
479+
480+
void
481+
fd_stake_ci_dest_remove( fd_stake_ci_t * info,
482+
fd_pubkey_t const * pubkey ) {
483+
ci_dest_remove_impl( info, pubkey, info->epoch_info+0UL );
484+
ci_dest_remove_impl( info, pubkey, info->epoch_info+1UL );
485+
486+
}
487+
393488

394489
fd_shred_dest_t *
395490
fd_stake_ci_get_sdest_for_slot( fd_stake_ci_t const * info,

src/disco/shred/fd_stake_ci.h

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,15 @@ fd_stake_ci_t * fd_stake_ci_join( void * mem );
9393
void * fd_stake_ci_leave ( fd_stake_ci_t * info );
9494
void * fd_stake_ci_delete( void * mem );
9595

96-
/* fd_stake_ci_stake_msg_{init, fini} are used to handle messages
96+
/* Frankendancer and Firedancer's Gossip impls follow different regimes
97+
for broadcasting Contact Infos. Firedancer employs an update-based
98+
regime where we receive update/remove messages for individual contact
99+
info entries. Frankendancer (and thusly Agave) performs a full table
100+
broadcast. fd_stake_ci offers two sets of APIs that cater to the
101+
different regimes. */
102+
103+
/* Frankendancer only:
104+
fd_stake_ci_stake_msg_{init, fini} are used to handle messages
97105
containing stake weight updates from the Rust side of the splice, and
98106
fd_stake_ci_dest_add_{init, fini} are used to handle messages
99107
containing contact info (potential shred destinations) updates from
@@ -152,6 +160,18 @@ void fd_stake_ci_stake_msg_fini( fd_stake_ci_t * info
152160
fd_shred_dest_weighted_t * fd_stake_ci_dest_add_init ( fd_stake_ci_t * info );
153161
void fd_stake_ci_dest_add_fini ( fd_stake_ci_t * info, ulong cnt );
154162

163+
/* Firedancer only:
164+
The full client's Gossip update model publishes individual contact
165+
info updates (update/insert or remove), which requires a different
166+
set of dest_ APIs.
167+
168+
fd_stake_ci_dest_update updates (or adds, if necessary) a shred dest
169+
entry. ip4 is in net order, port is in host order and are both
170+
assumed to be non-zero. */
171+
172+
void fd_stake_ci_dest_update( fd_stake_ci_t * info, fd_pubkey_t const * pubkey, uint ip4, ushort port );
173+
void fd_stake_ci_dest_remove( fd_stake_ci_t * info, fd_pubkey_t const * pubkey );
174+
155175

156176
/* fd_stake_ci_set_identity changes the identity of the locally running
157177
validator at runtime. */

src/disco/shred/test_stake_ci.c

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,206 @@ test_staked_by_vote( void ) {
445445
fd_stake_ci_delete( fd_stake_ci_leave( info ) );
446446
}
447447

448+
static void
449+
test_dest_update( void ) {
450+
fd_stake_ci_t * info = fd_stake_ci_join( fd_stake_ci_new( _info, identity_key ) );
451+
452+
/* Set up initial state with only staked nodes */
453+
fd_stake_ci_stake_msg_init( info, generate_stake_msg( stake_msg, 0UL, "ABC" ) );
454+
fd_stake_ci_stake_msg_fini( info );
455+
check_destinations( info, 0UL, "ABC", "I" );
456+
457+
/* Test updating existing staked node */
458+
fd_pubkey_t pubkey_a;
459+
memset( pubkey_a.uc, 'A', sizeof(fd_pubkey_t) );
460+
fd_stake_ci_dest_update( info, &pubkey_a, 0x12345678U, 8080 );
461+
462+
fd_shred_dest_t * sdest = fd_stake_ci_get_sdest_for_slot( info, 0UL );
463+
fd_shred_dest_idx_t idx_a = fd_shred_dest_pubkey_to_idx( sdest, &pubkey_a );
464+
FD_TEST( idx_a != FD_SHRED_DEST_NO_DEST );
465+
fd_shred_dest_weighted_t * dest_a = fd_shred_dest_idx_to_dest( sdest, idx_a );
466+
FD_TEST( dest_a->ip4 == 0x12345678U );
467+
FD_TEST( dest_a->port == 8080 );
468+
FD_TEST( dest_a->stake_lamports > 0UL ); /* Should still be staked */
469+
470+
/* Test adding new unstaked node via update */
471+
fd_pubkey_t pubkey_d;
472+
memset( pubkey_d.uc, 'D', sizeof(fd_pubkey_t) );
473+
fd_stake_ci_dest_update( info, &pubkey_d, 0x87654321U, 9090 );
474+
475+
/* D should now be in the unstaked list */
476+
check_destinations( info, 0UL, "ABC", "DI" );
477+
478+
fd_shred_dest_idx_t idx_d = fd_shred_dest_pubkey_to_idx( sdest, &pubkey_d );
479+
FD_TEST( idx_d != FD_SHRED_DEST_NO_DEST );
480+
fd_shred_dest_weighted_t * dest_d = fd_shred_dest_idx_to_dest( sdest, idx_d );
481+
FD_TEST( dest_d->ip4 == 0x87654321U );
482+
FD_TEST( dest_d->port == 9090 );
483+
FD_TEST( dest_d->stake_lamports == 0UL ); /* Should be unstaked */
484+
485+
/* Test adding multiple new unstaked nodes via update */
486+
fd_pubkey_t pubkey_e, pubkey_f;
487+
memset( pubkey_e.uc, 'E', sizeof(fd_pubkey_t) );
488+
memset( pubkey_f.uc, 'F', sizeof(fd_pubkey_t) );
489+
490+
fd_stake_ci_dest_update( info, &pubkey_e, 0x11111111U, 1111 );
491+
fd_stake_ci_dest_update( info, &pubkey_f, 0x22222222U, 2222 );
492+
493+
/* Check that E and F were added as unstaked */
494+
check_destinations( info, 0UL, "ABC", "DEFI" );
495+
496+
/* Test updating an unstaked node's contact info */
497+
fd_stake_ci_dest_update( info, &pubkey_d, 0x99999999U, 9999 );
498+
499+
idx_d = fd_shred_dest_pubkey_to_idx( sdest, &pubkey_d );
500+
dest_d = fd_shred_dest_idx_to_dest( sdest, idx_d );
501+
FD_TEST( dest_d->ip4 == 0x99999999U );
502+
FD_TEST( dest_d->port == 9999 );
503+
FD_TEST( dest_d->stake_lamports == 0UL ); /* Should still be unstaked */
504+
505+
/* Test that updates apply to both epochs */
506+
fd_stake_ci_stake_msg_init( info, generate_stake_msg( stake_msg, 1UL, "AB" ) );
507+
fd_stake_ci_stake_msg_fini( info );
508+
509+
/* Update should affect both epoch 0 and epoch 1 */
510+
fd_pubkey_t pubkey_b;
511+
memset( pubkey_b.uc, 'B', sizeof(fd_pubkey_t) );
512+
fd_stake_ci_dest_update( info, &pubkey_b, 0x11223344U, 5555 );
513+
514+
/* Check epoch 0 */
515+
fd_shred_dest_t * sdest0 = fd_stake_ci_get_sdest_for_slot( info, 0UL );
516+
fd_shred_dest_idx_t idx_b0 = fd_shred_dest_pubkey_to_idx( sdest0, &pubkey_b );
517+
fd_shred_dest_weighted_t * dest_b0 = fd_shred_dest_idx_to_dest( sdest0, idx_b0 );
518+
FD_TEST( dest_b0->ip4 == 0x11223344U );
519+
FD_TEST( dest_b0->port == 5555 );
520+
521+
/* Check epoch 1 */
522+
fd_shred_dest_t * sdest1 = fd_stake_ci_get_sdest_for_slot( info, 1000UL );
523+
fd_shred_dest_idx_t idx_b1 = fd_shred_dest_pubkey_to_idx( sdest1, &pubkey_b );
524+
fd_shred_dest_weighted_t * dest_b1 = fd_shred_dest_idx_to_dest( sdest1, idx_b1 );
525+
FD_TEST( dest_b1->ip4 == 0x11223344U );
526+
FD_TEST( dest_b1->port == 5555 );
527+
528+
/* Test adding a new node that gets added to both epochs */
529+
fd_pubkey_t pubkey_z;
530+
memset( pubkey_z.uc, 'Z', sizeof(fd_pubkey_t) );
531+
fd_stake_ci_dest_update( info, &pubkey_z, 0xAABBCCDDU, 7777 );
532+
533+
/* Z should be unstaked in both epochs */
534+
check_destinations( info, 0UL, "ABC", "DEFIZ" );
535+
/* C moved from staked to unstaked in epoch 1,
536+
and with no contact info entry it should not
537+
appear in the unstaked list. */
538+
check_destinations( info, 1UL, "AB", "DEFIZ" );
539+
540+
fd_pubkey_t pubkey_c;
541+
memset( pubkey_c.uc, 'C', sizeof(fd_pubkey_t) );
542+
fd_stake_ci_dest_update( info, &pubkey_c, 0xCCCCCCCCU, 8888 );
543+
544+
/* C should now be unstaked and present in epoch 1 */
545+
check_destinations( info, 1UL, "AB", "CDEFIZ" );
546+
547+
548+
fd_stake_ci_delete( fd_stake_ci_leave( info ) );
549+
}
550+
551+
static void
552+
test_dest_remove( void ) {
553+
fd_stake_ci_t * info = fd_stake_ci_join( fd_stake_ci_new( _info, identity_key ) );
554+
555+
/* Set up initial state with some staked and unstaked nodes */
556+
fd_stake_ci_stake_msg_init( info, generate_stake_msg( stake_msg, 0UL, "ABC" ) );
557+
fd_stake_ci_stake_msg_fini( info );
558+
559+
/* Build up destination list using only update operations */
560+
fd_pubkey_t pubkey_a, pubkey_b, pubkey_c, pubkey_d, pubkey_e, pubkey_f;
561+
memset( pubkey_a.uc, 'A', sizeof(fd_pubkey_t) );
562+
memset( pubkey_b.uc, 'B', sizeof(fd_pubkey_t) );
563+
memset( pubkey_c.uc, 'C', sizeof(fd_pubkey_t) );
564+
memset( pubkey_d.uc, 'D', sizeof(fd_pubkey_t) );
565+
memset( pubkey_e.uc, 'E', sizeof(fd_pubkey_t) );
566+
memset( pubkey_f.uc, 'F', sizeof(fd_pubkey_t) );
567+
568+
/* Update staked nodes A, B, C with contact info */
569+
fd_stake_ci_dest_update( info, &pubkey_a, 0x11111111U, 1111 );
570+
fd_stake_ci_dest_update( info, &pubkey_b, 0x22222222U, 2222 );
571+
fd_stake_ci_dest_update( info, &pubkey_c, 0x33333333U, 3333 );
572+
573+
/* Add unstaked nodes D, E, F via update */
574+
fd_stake_ci_dest_update( info, &pubkey_d, 0x44444444U, 4444 );
575+
fd_stake_ci_dest_update( info, &pubkey_e, 0x55555555U, 5555 );
576+
fd_stake_ci_dest_update( info, &pubkey_f, 0x66666666U, 6666 );
577+
578+
579+
check_destinations( info, 0UL, "ABC", "DEFI" );
580+
581+
/* Test removing unstaked node */
582+
memset( pubkey_d.uc, 'D', sizeof(fd_pubkey_t) );
583+
fd_stake_ci_dest_remove( info, &pubkey_d );
584+
585+
/* D should be removed from unstaked list */
586+
check_destinations( info, 0UL, "ABC", "EFI" );
587+
588+
fd_shred_dest_t * sdest = fd_stake_ci_get_sdest_for_slot( info, 0UL );
589+
fd_shred_dest_idx_t idx_d = fd_shred_dest_pubkey_to_idx( sdest, &pubkey_d );
590+
FD_TEST( idx_d == FD_SHRED_DEST_NO_DEST ); /* Should not be found */
591+
592+
/* Test removing staked node (should NOT actually remove it, just clear contact info) */
593+
memset( pubkey_a.uc, 'A', sizeof(fd_pubkey_t) );
594+
595+
/* First update A with some contact info */
596+
fd_stake_ci_dest_update( info, &pubkey_a, 0x12345678U, 8080 );
597+
fd_shred_dest_idx_t idx_a = fd_shred_dest_pubkey_to_idx( sdest, &pubkey_a );
598+
fd_shred_dest_weighted_t * dest_a = fd_shred_dest_idx_to_dest( sdest, idx_a );
599+
FD_TEST( dest_a->ip4 == 0x12345678U );
600+
FD_TEST( dest_a->port == 8080 );
601+
602+
/* Now try to remove A - it should stay because it's staked */
603+
fd_stake_ci_dest_remove( info, &pubkey_a );
604+
605+
/* A should still be in staked list */
606+
check_destinations( info, 0UL, "ABC", "EFI" );
607+
idx_a = fd_shred_dest_pubkey_to_idx( sdest, &pubkey_a );
608+
FD_TEST( idx_a != FD_SHRED_DEST_NO_DEST ); /* Should still be found */
609+
dest_a = fd_shred_dest_idx_to_dest( sdest, idx_a );
610+
FD_TEST( dest_a->stake_lamports > 0UL ); /* Should still be staked */
611+
612+
/* Test removing non-existing node (should be no-op) */
613+
fd_pubkey_t pubkey_z;
614+
memset( pubkey_z.uc, 'Z', sizeof(fd_pubkey_t) );
615+
fd_stake_ci_dest_remove( info, &pubkey_z );
616+
617+
/* Nothing should change */
618+
check_destinations( info, 0UL, "ABC", "EFI" );
619+
620+
/* Test that removes apply to both epochs */
621+
fd_stake_ci_stake_msg_init( info, generate_stake_msg( stake_msg, 1UL, "AB" ) );
622+
fd_stake_ci_stake_msg_fini( info );
623+
624+
/* E should be unstaked in both epochs, remove it */
625+
memset( pubkey_e.uc, 'E', sizeof(fd_pubkey_t) );
626+
fd_stake_ci_dest_remove( info, &pubkey_e );
627+
628+
/* Check both epochs - E should be gone from both */
629+
check_destinations( info, 0UL, "ABC", "FI" );
630+
check_destinations( info, 1UL, "AB", "CFI" );
631+
632+
/* Verify E is not found in either epoch */
633+
fd_shred_dest_t * sdest0 = fd_stake_ci_get_sdest_for_slot( info, 0UL );
634+
fd_shred_dest_t * sdest1 = fd_stake_ci_get_sdest_for_slot( info, 1000UL );
635+
FD_TEST( fd_shred_dest_pubkey_to_idx( sdest0, &pubkey_e ) == FD_SHRED_DEST_NO_DEST );
636+
FD_TEST( fd_shred_dest_pubkey_to_idx( sdest1, &pubkey_e ) == FD_SHRED_DEST_NO_DEST );
637+
638+
/* Test removing multiple unstaked nodes */
639+
memset( pubkey_f.uc, 'F', sizeof(fd_pubkey_t) );
640+
fd_stake_ci_dest_remove( info, &pubkey_f );
641+
642+
check_destinations( info, 0UL, "ABC", "I" );
643+
check_destinations( info, 1UL, "AB", "CI" );
644+
645+
fd_stake_ci_delete( fd_stake_ci_leave( info ) );
646+
}
647+
448648
int
449649
main( int argc,
450650
char ** argv ) {
@@ -473,6 +673,9 @@ main( int argc,
473673
test_set_identity();
474674
test_staked_by_vote();
475675

676+
test_dest_update();
677+
test_dest_remove();
678+
476679
FD_LOG_NOTICE(( "pass" ));
477680
fd_halt();
478681
return 0;

0 commit comments

Comments
 (0)