Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/app/firedancer/topology.c
Original file line number Diff line number Diff line change
Expand Up @@ -1615,6 +1615,10 @@ fd_topo_configure_tile( fd_topo_tile_t * tile,
tile->votor.ip_addr = config->net.ip_addr;
tile->votor.max_live_slots = config->firedancer.runtime.max_live_slots;
fd_cstr_ncpy( tile->votor.identity_key_path, config->paths.identity_key, sizeof(tile->votor.identity_key_path) );
tile->votor.authorized_voter_paths_cnt = config->firedancer.paths.authorized_voter_paths_cnt;
for( ulong i=0UL; i<tile->votor.authorized_voter_paths_cnt; i++ ) {
fd_cstr_ncpy( tile->votor.authorized_voter_paths[ i ], config->firedancer.paths.authorized_voter_paths[ i ], sizeof(tile->votor.authorized_voter_paths[ i ]) );
}

} else if( FD_UNLIKELY( !strcmp( tile->name, "tower" ) ) ) {
tile->tower.authorized_voter_paths_cnt = config->firedancer.paths.authorized_voter_paths_cnt;
Expand Down
36 changes: 29 additions & 7 deletions src/choreo/votor/ag_votor.c
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ struct __attribute__((aligned(128UL))) ag_votor {
ulong curr_epoch_slot;
ulong next_epoch_rank;
ulong next_epoch_slot;
ag_bls_sec_t bls_sec;
uchar const * curr_bls_sec;
uchar const * next_bls_sec;

ag_event_vote_t * vote_events;
ag_event_cert_t * cert_events;
Expand Down Expand Up @@ -246,29 +247,40 @@ own_rank( ag_votor_t const * self,
return (ushort)fd_ulong_if( slot>=self->next_epoch_slot, self->next_epoch_rank, self->curr_epoch_rank );
}

static uchar const *
own_bls_sec( ag_votor_t const * self,
ulong slot ) {
return fd_ptr_if( slot>=self->next_epoch_slot, self->next_bls_sec, self->curr_bls_sec );
}

void
ag_votor_advance_epoch( ag_votor_t * self,
ulong epoch_rank,
ulong epoch_slot ) {
if( FD_UNLIKELY( self->curr_epoch_slot==ULONG_MAX ) ) {
self->curr_epoch_rank = epoch_rank;
self->curr_epoch_slot = epoch_slot;
self->curr_bls_sec = NULL;
} else if( FD_UNLIKELY( self->next_epoch_slot==ULONG_MAX ) ) {
self->next_epoch_rank = epoch_rank;
self->next_epoch_slot = epoch_slot;
self->next_bls_sec = NULL;
} else {
self->curr_epoch_rank = self->next_epoch_rank;
self->curr_epoch_slot = self->next_epoch_slot;
self->curr_bls_sec = self->next_bls_sec;
self->next_epoch_rank = epoch_rank;
self->next_epoch_slot = epoch_slot;
self->next_bls_sec = NULL;
}
}

void
ag_votor_set_bls_key( ag_votor_t * self,
ag_bls_sec_t const bls_key ) {
FD_TEST( bls_key );
memcpy( self->bls_sec, bls_key, AG_BLS_SEC_SZ );
if( FD_LIKELY( self->next_epoch_slot==ULONG_MAX ) ) self->curr_bls_sec = bls_key;
else self->next_bls_sec = bls_key;
}

void
Expand Down Expand Up @@ -398,13 +410,15 @@ try_final( ag_votor_t * self,
ulong slot,
ag_block_hash_t const hash ) {
FD_TEST( slot>=first_unpruned_slot( self ) );
uchar const * bls_sec = own_bls_sec( self, slot );
if( FD_UNLIKELY( !bls_sec ) ) return;

slot_state_ele_t const * state = slot_state_map_ele_query_const( self->slot_states->map, &slot, NULL, self->slot_states->pool );
int notarized = state && state->block_notarized && !memcmp( state->block_notarized_hash, hash, sizeof(ag_block_hash_t) );
int voted_notar = state && state->voted_notar && !memcmp( state->voted_notar_hash, hash, sizeof(ag_block_hash_t) );
int not_bad = !( state && state->bad_window );
if( FD_LIKELY( notarized && voted_notar && not_bad ) ) {
ag_vote_t vote = ag_vote_construct_final( slot, self->bls_sec, own_rank( self, slot ), self->shred_version );
ag_vote_t vote = ag_vote_construct_final( slot, bls_sec, own_rank( self, slot ), self->shred_version );
FD_TEST( !vote_events_full( self->vote_events ) );
vote_events_push( self->vote_events, (ag_event_vote_t){ .seq = self->seq++, .ts = self->now, .vote = vote } );
state_mut( self, slot )->retired = 1;
Expand All @@ -416,6 +430,8 @@ try_notar( ag_votor_t * self,
ulong slot,
ag_block_info_t const * block_info ) {
FD_TEST( slot>=first_unpruned_slot( self ) );
uchar const * bls_sec = own_bls_sec( self, slot );
if( FD_UNLIKELY( !bls_sec ) ) return 0;
if( FD_UNLIKELY( has_voted( self, slot ) ) ) return 0;

ag_block_hash_t hash;
Expand All @@ -438,7 +454,7 @@ try_notar( ag_votor_t * self,
if( FD_UNLIKELY( memcmp( parent_state->voted_notar_hash, parent.hash, sizeof(ag_block_hash_t) )!=0 ) ) return 0;
}

ag_vote_t vote = ag_vote_construct_notar( slot, hash, self->bls_sec, own_rank( self, slot ), self->shred_version );
ag_vote_t vote = ag_vote_construct_notar( slot, hash, bls_sec, own_rank( self, slot ), self->shred_version );
FD_TEST( !vote_events_full( self->vote_events ) );
vote_events_push( self->vote_events, (ag_event_vote_t){ .seq = self->seq++, .ts = self->now, .vote = vote } );

Expand All @@ -461,12 +477,14 @@ try_skip_window( ag_votor_t * self,
ulong window_start = ag_first_slot_in_window( slot );
for( ulong s=window_start; s<window_start+AG_SLOTS_PER_WINDOW; s++ ) {
if( FD_UNLIKELY( has_voted( self, s ) ) ) continue;
uchar const * bls_sec = own_bls_sec( self, s );
if( FD_UNLIKELY( !bls_sec ) ) continue;

slot_state_ele_t * state = state_mut( self, s );
state->voted = 1;
state->bad_window = 1;

ag_vote_t vote = ag_vote_construct_skip( s, self->bls_sec, own_rank( self, s ), self->shred_version );
ag_vote_t vote = ag_vote_construct_skip( s, bls_sec, own_rank( self, s ), self->shred_version );
FD_TEST( !vote_events_full( self->vote_events ) );
vote_events_push( self->vote_events, (ag_event_vote_t){ .seq = self->seq++, .ts = self->now, .vote = vote } );
}
Expand Down Expand Up @@ -577,8 +595,10 @@ ag_votor_handle_pool_event( ag_votor_t * self,
case AG_EVENT_POOL_SAFE_TO_NOTAR: {
ulong slot = event->safe_to_notar.slot;
uchar const * hash = event->safe_to_notar.hash;
uchar const * bls_sec = own_bls_sec( self, slot );
if( FD_UNLIKELY( !bls_sec ) ) break;

ag_vote_t vote = ag_vote_construct_notar_fallback( slot, hash, self->bls_sec, own_rank( self, slot ), self->shred_version );
ag_vote_t vote = ag_vote_construct_notar_fallback( slot, hash, bls_sec, own_rank( self, slot ), self->shred_version );
FD_TEST( !vote_events_full( self->vote_events ) );
vote_events_push( self->vote_events, (ag_event_vote_t){ .seq = self->seq++, .ts = self->now, .vote = vote } );
try_skip_window( self, slot );
Expand All @@ -588,8 +608,10 @@ ag_votor_handle_pool_event( ag_votor_t * self,

case AG_EVENT_POOL_SAFE_TO_SKIP: {
ulong slot = event->safe_to_skip;
uchar const * bls_sec = own_bls_sec( self, slot );
if( FD_UNLIKELY( !bls_sec ) ) break;

ag_vote_t vote = ag_vote_construct_skip_fallback( slot, self->bls_sec, own_rank( self, slot ), self->shred_version );
ag_vote_t vote = ag_vote_construct_skip_fallback( slot, bls_sec, own_rank( self, slot ), self->shred_version );
FD_TEST( !vote_events_full( self->vote_events ) );
vote_events_push( self->vote_events, (ag_event_vote_t){ .seq = self->seq++, .ts = self->now, .vote = vote } );
try_skip_window( self, slot );
Expand Down
5 changes: 5 additions & 0 deletions src/choreo/votor/ag_votor.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ ag_votor_advance_epoch( ag_votor_t * self,
ulong epoch_rank,
ulong epoch_slot );

/* Sets the signing key for the most recently advanced epoch. If this
is not called after advancing an epoch, the votor does not vote in
that epoch. The caller retains ownership of bls_key and must keep it
valid for the lifetime of the votor. */

void
ag_votor_set_bls_key( ag_votor_t * self,
ag_bls_sec_t const bls_key );
Expand Down
49 changes: 49 additions & 0 deletions src/choreo/votor/test_ag_votor.c
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,53 @@ test_safe_to_skip( void ) {
teardown_votor( votor );
}

static void
test_bls_key_rotates_with_epoch( void ) {
ag_votor_t * votor = setup_votor( 0L );

ag_votor_advance_epoch( votor, 0UL, 2UL );
ag_votor_set_bls_key ( votor, g_sk[1] );

ag_block_id_t parent = genesis_block_id();
ag_vote_t current_vote = send_block_and_expect_notar( votor, 1UL, &parent );
FD_TEST( ag_vote_verify( &current_vote, g_info[0].bls_key, TEST_SHRED_VERSION ) );
FD_TEST( !ag_vote_verify( &current_vote, g_info[1].bls_key, TEST_SHRED_VERSION ) );

parent.slot = 1UL;
memcpy( parent.hash, ag_vote_notar_block_hash( &current_vote.notar ), sizeof(ag_block_hash_t) );
ag_vote_t next_vote = send_block_and_expect_notar( votor, 2UL, &parent );
FD_TEST( !ag_vote_verify( &next_vote, g_info[0].bls_key, TEST_SHRED_VERSION ) );
FD_TEST( ag_vote_verify( &next_vote, g_info[1].bls_key, TEST_SHRED_VERSION ) );

teardown_votor( votor );
}

static void
test_missing_bls_key_disables_voting( void ) {
ag_votor_t * votor = setup_votor( 0L );

ag_votor_advance_epoch( votor, 0UL, 2UL );

ag_block_id_t parent = genesis_block_id();
ag_vote_t current_vote = send_block_and_expect_notar( votor, 1UL, &parent );

parent.slot = 1UL;
memcpy( parent.hash, ag_vote_notar_block_hash( &current_vote.notar ), sizeof(ag_block_hash_t) );

ag_event_block_t first_shred = { .kind = AG_EVENT_BLOCK_FIRST_SHRED, .slot = 2UL };
ag_votor_handle_block_event( votor, &first_shred );

ag_event_replay_t block = { .kind = AG_EVENT_REPLAY_COMPLETED };
block.slot = 2UL;
block.block_info.parent = parent;
random_hash( block.block_info.hash );
ag_votor_handle_replay_event( votor, &block );

FD_TEST_NO_MSG( votor );

teardown_votor( votor );
}

/* src/consensus/votor.rs::prunes_to_finalized_window */

static void
Expand Down Expand Up @@ -395,6 +442,8 @@ main( int argc,
test_pending_block_not_notarized_after_skip();
test_safe_to_notar();
test_safe_to_skip();
test_bls_key_rotates_with_epoch();
test_missing_bls_key_disables_voting();
test_prunes_to_finalized_window();

FD_LOG_NOTICE(( "pass" ));
Expand Down
2 changes: 2 additions & 0 deletions src/disco/topo/fd_topo.h
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,8 @@ struct fd_topo_tile {

struct {
char identity_key_path[ PATH_MAX ];
ulong authorized_voter_paths_cnt;
char authorized_voter_paths[ 16 ][ PATH_MAX ];
ushort quic_client_listen_port;
ushort quic_server_listen_port;
uint ip_addr;
Expand Down
59 changes: 46 additions & 13 deletions src/discof/votor/fd_votor_tile.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#define OUT_IDX_NET (1UL)

#define QUIC_CONN_MAX (AG_VAT_MAX * 2)
#define BLS_KEY_MAX (17UL) /* identity plus up to 16 configured authorized voters */

#define CLOSE_CODE_INVALID_IDENTITY (2U)
#define CLOSE_CODE_NOT_ADMITTED (3U)
Expand Down Expand Up @@ -159,15 +160,23 @@ typedef struct peer peer_t;
#define MAP_MEMOIZE 0
#include "../../util/tmpl/fd_map.c"

struct derived_bls_key {
ag_bls_sec_t sec;
ag_bls_pub_t pub;
};
typedef struct derived_bls_key derived_bls_key_t;
FD_STATIC_ASSERT( sizeof(derived_bls_key_t)*BLS_KEY_MAX<=4096UL, derived_bls_keys_fit_protected_page );

struct fd_votor_tile {

/* Metadata */

uchar const * identity_keypair; /* FIXME keyguard */
fd_pubkey_t id_key;
ag_bls_sec_t bls_key;
uchar sha512[ FD_SHA512_FOOTPRINT ] __attribute__((aligned(FD_SHA512_ALIGN)));
ushort shred_version;
uchar const * identity_keypair; /* FIXME keyguard */
fd_pubkey_t id_key;
derived_bls_key_t const * bls_keys;
ulong bls_key_cnt;
uchar sha512[ FD_SHA512_FOOTPRINT ] __attribute__((aligned(FD_SHA512_ALIGN)));
ushort shred_version;

/* Data */

Expand Down Expand Up @@ -596,6 +605,21 @@ handle_epoch( fd_votor_tile_t * ctx,
ag_pool_advance_epoch( ctx->pool, epoch_info, epoch_rank, msg->start_slot );
ag_votor_advance_epoch( ctx->votor, epoch_rank, msg->start_slot );

if( FD_LIKELY( epoch_rank!=USHORT_MAX ) ) {
uchar const * registered_key = epoch_info->validators[ epoch_rank ].bls_key;
ulong key_idx = 0UL;
for( ; key_idx<ctx->bls_key_cnt; key_idx++ ) {
if( FD_LIKELY( !memcmp( ctx->bls_keys[ key_idx ].pub, registered_key, AG_BLS_PUB_SZ ) ) ) {
ag_votor_set_bls_key( ctx->votor, ctx->bls_keys[ key_idx ].sec );
break;
}
}
if( FD_UNLIKELY( key_idx==ctx->bls_key_cnt ) ) {
FD_LOG_WARNING(( "votor epoch %lu: no configured authorized voter derives the BLS key registered for our identity; voting is disabled for this epoch",
msg->epoch ));
}
}

/* update our leader schedule. msg only points into the epoch dcache
for this callback, so it must be consumed here. */

Expand Down Expand Up @@ -1031,14 +1055,24 @@ privileged_init( fd_topo_t const * topo,
ctx->identity_keypair = fd_keyload_load( tile->votor.identity_key_path, 0 );
memcpy( ctx->id_key.uc, ctx->identity_keypair+32UL, sizeof(fd_pubkey_t) );

char const derive_msg[] = "bls-key-derive-alpenglow";
uchar ikm[ 64 ];
fd_sha512_t _sha[ 1 ];
fd_sha512_t * sha = fd_sha512_join( fd_sha512_new( _sha ) );
fd_ed25519_sign( ikm, (uchar const *)derive_msg, sizeof(derive_msg)-1UL, ctx->identity_keypair+32UL, ctx->identity_keypair, sha );
FD_TEST( tile->votor.authorized_voter_paths_cnt+1UL<=BLS_KEY_MAX );
ctx->bls_key_cnt = tile->votor.authorized_voter_paths_cnt+1UL;
derived_bls_key_t * bls_keys = fd_keyload_alloc_protected_pages( 1UL, 2UL );

static char const derive_msg[] = "bls-key-derive-alpenglow";
uchar ikm[ 64 ];
fd_sha512_t _sha[ 1 ];
fd_sha512_t * sha = fd_sha512_join( fd_sha512_new( _sha ) );
for( ulong i=0UL; i<ctx->bls_key_cnt; i++ ) {
uchar const * keypair = i ? fd_keyload_load( tile->votor.authorized_voter_paths[ i-1UL ], 0 ) : ctx->identity_keypair;
fd_ed25519_sign( ikm, (uchar const *)derive_msg, sizeof(derive_msg)-1UL, keypair+32UL, keypair, sha );
ag_bls_sec_derive( bls_keys[ i ].sec, ikm, sizeof(ikm) );
ag_bls_sec_to_pub( bls_keys[ i ].sec, bls_keys[ i ].pub );
fd_memzero_explicit( ikm, sizeof(ikm) );
if( FD_LIKELY( i ) ) fd_keyload_unload( keypair, 0 );
}
fd_sha512_leave( sha );
ag_bls_sec_derive( ctx->bls_key, ikm, sizeof(ikm) );
fd_memzero_explicit( ikm, sizeof(ikm) );
ctx->bls_keys = (derived_bls_key_t const *)fd_keyload_mprotect_ro( (uchar *)bls_keys, 0 );

fd_log_wallclock();
}
Expand Down Expand Up @@ -1078,7 +1112,6 @@ unprivileged_init( fd_topo_t const * topo,

ctx->votor = ag_votor_join( ag_votor_new( votor, tile->votor.max_live_slots, seed ) );
FD_TEST( ctx->votor );
ag_votor_set_bls_key( ctx->votor, ctx->bls_key );

ctx->curr_epoch_info = NULL;
ctx->curr_epoch_slot = ULONG_MAX;
Expand Down
Loading