From 3544ee8247d0cc6252871ed26d9d8847033b239c Mon Sep 17 00:00:00 2001 From: Ishan Bhatt Date: Thu, 3 Sep 2026 19:14:52 +0000 Subject: [PATCH 1/2] alpenglow: derive bls pubkeys from auth voters --- src/app/firedancer/topology.c | 4 +++ src/choreo/votor/ag_votor.c | 36 +++++++++++++++---- src/choreo/votor/ag_votor.h | 5 +++ src/choreo/votor/test_ag_votor.c | 49 ++++++++++++++++++++++++++ src/disco/topo/fd_topo.h | 2 ++ src/discof/votor/fd_votor_tile.c | 59 +++++++++++++++++++++++++------- 6 files changed, 135 insertions(+), 20 deletions(-) diff --git a/src/app/firedancer/topology.c b/src/app/firedancer/topology.c index 4e669eaf8bb..225ca11b272 100644 --- a/src/app/firedancer/topology.c +++ b/src/app/firedancer/topology.c @@ -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; ivotor.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; diff --git a/src/choreo/votor/ag_votor.c b/src/choreo/votor/ag_votor.c index 4aaffe25aaa..87483723fae 100644 --- a/src/choreo/votor/ag_votor.c +++ b/src/choreo/votor/ag_votor.c @@ -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; @@ -246,6 +247,12 @@ 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, @@ -253,14 +260,18 @@ ag_votor_advance_epoch( ag_votor_t * self, 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; } } @@ -268,7 +279,8 @@ 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 @@ -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; @@ -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; @@ -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 } ); @@ -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; svoted = 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 } ); } @@ -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 ); @@ -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 ); diff --git a/src/choreo/votor/ag_votor.h b/src/choreo/votor/ag_votor.h index 723631af8d1..3c024b26883 100644 --- a/src/choreo/votor/ag_votor.h +++ b/src/choreo/votor/ag_votor.h @@ -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 ); diff --git a/src/choreo/votor/test_ag_votor.c b/src/choreo/votor/test_ag_votor.c index 0d03a3de477..0e0288e6cd7 100644 --- a/src/choreo/votor/test_ag_votor.c +++ b/src/choreo/votor/test_ag_votor.c @@ -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( ¤t_vote, g_info[0].bls_key, TEST_SHRED_VERSION ) ); + FD_TEST( !ag_vote_verify( ¤t_vote, g_info[1].bls_key, TEST_SHRED_VERSION ) ); + + parent.slot = 1UL; + memcpy( parent.hash, ag_vote_notar_block_hash( ¤t_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( ¤t_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 @@ -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" )); diff --git a/src/disco/topo/fd_topo.h b/src/disco/topo/fd_topo.h index 8dec910a81a..6202f23402e 100644 --- a/src/disco/topo/fd_topo.h +++ b/src/disco/topo/fd_topo.h @@ -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; diff --git a/src/discof/votor/fd_votor_tile.c b/src/discof/votor/fd_votor_tile.c index 6b7644508e4..7933f28863f 100644 --- a/src/discof/votor/fd_votor_tile.c +++ b/src/discof/votor/fd_votor_tile.c @@ -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) @@ -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 */ @@ -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_idxbls_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. */ @@ -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; ibls_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(); } @@ -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; From b8b736dca759e08999670cce8caf2632b03a4b87 Mon Sep 17 00:00:00 2001 From: Ishan Bhatt Date: Mon, 14 Sep 2026 14:02:17 +0000 Subject: [PATCH 2/2] wip --- src/choreo/votor/ag_vote.c | 18 +++-- src/choreo/votor/ag_vote.h | 10 +++ src/choreo/votor/ag_votor.c | 55 ++++++++++++-- src/choreo/votor/ag_votor.h | 8 ++ src/choreo/votor/test_ag_cert_builder.h | 57 ++++++++++++++ src/choreo/votor/test_ag_vote.c | 25 ++++--- src/choreo/votor/test_ag_votor.c | 95 +++++++++++++++++++++++- src/disco/keyguard/Local.mk | 2 +- src/disco/keyguard/fd_keyguard.h | 19 +++-- src/disco/keyguard/fd_keyguard_bls.c | 54 ++++++++++++++ src/disco/keyguard/fd_keyguard_bls.h | 49 +++++++++++++ src/disco/keyguard/fd_keyguard_client.c | 17 ++++- src/disco/keyguard/fd_keyguard_client.h | 20 ++++- src/disco/keyguard/fd_keyguard_match.c | 9 ++- src/disco/keyguard/fd_sign_tile.c | 33 ++++++--- src/disco/keyguard/test_keyguard.c | 98 ++++++++++++++++++++++++- src/discof/votor/fd_votor_tile.c | 8 +- 17 files changed, 521 insertions(+), 56 deletions(-) create mode 100644 src/disco/keyguard/fd_keyguard_bls.c create mode 100644 src/disco/keyguard/fd_keyguard_bls.h diff --git a/src/choreo/votor/ag_vote.c b/src/choreo/votor/ag_vote.c index 681537b297f..626055b5393 100644 --- a/src/choreo/votor/ag_vote.c +++ b/src/choreo/votor/ag_vote.c @@ -4,16 +4,18 @@ static void sign( ag_vote_t const * self, fd_bls_sign_fn sign_fn, void * sign_ctx, + uchar const * public_key, ushort shred_version, fd_bls_sig_t * sig ) { uchar buf[ AG_VOTE_SIGNING_SER_MAX ]; ulong sz = ag_vote_signing_ser( self->kind, ag_vote_slot( self ), ag_vote_block_hash( self ), shred_version, buf ); - sign_fn( sign_ctx, sig, buf, sz ); + sign_fn( sign_ctx, sig, public_key, buf, sz ); } ag_vote_t ag_vote_construct_notar( fd_bls_sign_fn sign_fn, void * sign_ctx, + uchar const * public_key, ulong slot, ag_block_hash_t const hash, ushort rank, @@ -24,13 +26,14 @@ ag_vote_construct_notar( fd_bls_sign_fn sign_fn, vote.notar.rank = rank; vote.notar.shred_version = shred_version; memcpy( vote.notar.block_hash, hash, sizeof(ag_block_hash_t) ); - sign( &vote, sign_fn, sign_ctx, shred_version, &vote.notar.sig ); + sign( &vote, sign_fn, sign_ctx, public_key, shred_version, &vote.notar.sig ); return vote; } ag_vote_t ag_vote_construct_final( fd_bls_sign_fn sign_fn, void * sign_ctx, + uchar const * public_key, ulong slot, ushort rank, ushort shred_version ) { @@ -39,13 +42,14 @@ ag_vote_construct_final( fd_bls_sign_fn sign_fn, vote.final.slot = slot; vote.final.rank = rank; vote.final.shred_version = shred_version; - sign( &vote, sign_fn, sign_ctx, shred_version, &vote.final.sig ); + sign( &vote, sign_fn, sign_ctx, public_key, shred_version, &vote.final.sig ); return vote; } ag_vote_t ag_vote_construct_skip( fd_bls_sign_fn sign_fn, void * sign_ctx, + uchar const * public_key, ulong slot, ushort rank, ushort shred_version ) { @@ -54,13 +58,14 @@ ag_vote_construct_skip( fd_bls_sign_fn sign_fn, vote.skip.slot = slot; vote.skip.rank = rank; vote.skip.shred_version = shred_version; - sign( &vote, sign_fn, sign_ctx, shred_version, &vote.skip.sig ); + sign( &vote, sign_fn, sign_ctx, public_key, shred_version, &vote.skip.sig ); return vote; } ag_vote_t ag_vote_construct_notar_fallback( fd_bls_sign_fn sign_fn, void * sign_ctx, + uchar const * public_key, ulong slot, ag_block_hash_t const hash, ushort rank, @@ -71,13 +76,14 @@ ag_vote_construct_notar_fallback( fd_bls_sign_fn sign_fn, vote.notar_fallback.rank = rank; vote.notar_fallback.shred_version = shred_version; memcpy( vote.notar_fallback.block_hash, hash, sizeof(ag_block_hash_t) ); - sign( &vote, sign_fn, sign_ctx, shred_version, &vote.notar_fallback.sig ); + sign( &vote, sign_fn, sign_ctx, public_key, shred_version, &vote.notar_fallback.sig ); return vote; } ag_vote_t ag_vote_construct_skip_fallback( fd_bls_sign_fn sign_fn, void * sign_ctx, + uchar const * public_key, ulong slot, ushort rank, ushort shred_version ) { @@ -86,7 +92,7 @@ ag_vote_construct_skip_fallback( fd_bls_sign_fn sign_fn, vote.skip_fallback.slot = slot; vote.skip_fallback.rank = rank; vote.skip_fallback.shred_version = shred_version; - sign( &vote, sign_fn, sign_ctx, shred_version, &vote.skip_fallback.sig ); + sign( &vote, sign_fn, sign_ctx, public_key, shred_version, &vote.skip_fallback.sig ); return vote; } diff --git a/src/choreo/votor/ag_vote.h b/src/choreo/votor/ag_vote.h index 27016e4b761..69fe2ccae8e 100644 --- a/src/choreo/votor/ag_vote.h +++ b/src/choreo/votor/ag_vote.h @@ -12,9 +12,14 @@ #define AG_VOTE_CSTR_MAX (256UL) +/* public_key is the canonical compressed BLS key selector. payload is + the exact consensus preimage; the signer must not include public_key + in the signed bytes. */ + typedef void (* fd_bls_sign_fn)( void * ctx, fd_bls_sig_t * sig, + uchar const * public_key, uchar const * payload, ulong payload_sz ); struct ag_vote_notar { @@ -142,6 +147,7 @@ ag_vote_set_rank( ag_vote_t * self, ag_vote_t ag_vote_construct_notar( fd_bls_sign_fn sign_fn, void * sign_ctx, + uchar const * public_key, ulong slot, ag_block_hash_t const hash, ushort rank, @@ -150,6 +156,7 @@ ag_vote_construct_notar( fd_bls_sign_fn sign_fn, ag_vote_t ag_vote_construct_final( fd_bls_sign_fn sign_fn, void * sign_ctx, + uchar const * public_key, ulong slot, ushort rank, ushort shred_version ); @@ -157,6 +164,7 @@ ag_vote_construct_final( fd_bls_sign_fn sign_fn, ag_vote_t ag_vote_construct_skip( fd_bls_sign_fn sign_fn, void * sign_ctx, + uchar const * public_key, ulong slot, ushort rank, ushort shred_version ); @@ -164,6 +172,7 @@ ag_vote_construct_skip( fd_bls_sign_fn sign_fn, ag_vote_t ag_vote_construct_notar_fallback( fd_bls_sign_fn sign_fn, void * sign_ctx, + uchar const * public_key, ulong slot, ag_block_hash_t const hash, ushort rank, @@ -172,6 +181,7 @@ ag_vote_construct_notar_fallback( fd_bls_sign_fn sign_fn, ag_vote_t ag_vote_construct_skip_fallback( fd_bls_sign_fn sign_fn, void * sign_ctx, + uchar const * public_key, ulong slot, ushort rank, ushort shred_version ); diff --git a/src/choreo/votor/ag_votor.c b/src/choreo/votor/ag_votor.c index 640d4628e5e..73030f12830 100644 --- a/src/choreo/votor/ag_votor.c +++ b/src/choreo/votor/ag_votor.c @@ -87,6 +87,10 @@ struct __attribute__((aligned(128UL))) ag_votor { ulong curr_epoch_slot; ulong next_epoch_rank; ulong next_epoch_slot; + uchar curr_bls_pubkey[ FD_BLS_PUB_COMPRESSED_SZ ]; + uchar next_bls_pubkey[ FD_BLS_PUB_COMPRESSED_SZ ]; + int has_curr_bls_pubkey; + int has_next_bls_pubkey; ag_event_vote_t * vote_events; ag_event_cert_t * cert_events; @@ -303,14 +307,31 @@ ag_votor_advance_epoch( ag_votor_t * self, if( FD_UNLIKELY( self->curr_epoch_slot==ULONG_MAX ) ) { self->curr_epoch_rank = epoch_rank; self->curr_epoch_slot = epoch_slot; + self->has_curr_bls_pubkey = 0; } else if( FD_UNLIKELY( self->next_epoch_slot==ULONG_MAX ) ) { self->next_epoch_rank = epoch_rank; self->next_epoch_slot = epoch_slot; + self->has_next_bls_pubkey = 0; } else { self->curr_epoch_rank = self->next_epoch_rank; self->curr_epoch_slot = self->next_epoch_slot; + memcpy( self->curr_bls_pubkey, self->next_bls_pubkey, FD_BLS_PUB_COMPRESSED_SZ ); + self->has_curr_bls_pubkey = self->has_next_bls_pubkey; self->next_epoch_rank = epoch_rank; self->next_epoch_slot = epoch_slot; + self->has_next_bls_pubkey = 0; + } +} + +void +ag_votor_set_bls_pubkey( ag_votor_t * self, + uchar const public_key[ static FD_BLS_PUB_COMPRESSED_SZ ] ) { + if( FD_LIKELY( self->next_epoch_slot==ULONG_MAX ) ) { + memcpy( self->curr_bls_pubkey, public_key, FD_BLS_PUB_COMPRESSED_SZ ); + self->has_curr_bls_pubkey = 1; + } else { + memcpy( self->next_bls_pubkey, public_key, FD_BLS_PUB_COMPRESSED_SZ ); + self->has_next_bls_pubkey = 1; } } @@ -320,6 +341,14 @@ ag_votor_set_shred_version( ag_votor_t * self, self->shred_version = shred_version; } +static uchar const * +own_bls_pubkey( ag_votor_t const * self, + ulong slot ) { + if( FD_UNLIKELY( slot>=self->next_epoch_slot ) ) + return self->has_next_bls_pubkey ? self->next_bls_pubkey : NULL; + return self->has_curr_bls_pubkey ? self->curr_bls_pubkey : NULL; +} + static ushort own_rank( ag_votor_t const * self, ulong slot ) { @@ -383,13 +412,15 @@ try_final( ag_votor_t * self, ulong slot, ag_block_hash_t const hash ) { FD_TEST( slot>=first_unpruned_slot( self ) ); + uchar const * public_key = own_bls_pubkey( self, slot ); + if( FD_UNLIKELY( !public_key ) ) 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( self->bls_sign_fn, self->bls_sign_ctx, slot, own_rank( self, slot ), self->shred_version ); + ag_vote_t vote = ag_vote_construct_final( self->bls_sign_fn, self->bls_sign_ctx, public_key, slot, 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; @@ -401,6 +432,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 * public_key = own_bls_pubkey( self, slot ); + if( FD_UNLIKELY( !public_key ) ) return 0; if( FD_UNLIKELY( has_voted( self, slot ) ) ) return 0; ag_block_hash_t hash; @@ -423,7 +456,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( self->bls_sign_fn, self->bls_sign_ctx, slot, hash, own_rank( self, slot ), self->shred_version ); + ag_vote_t vote = ag_vote_construct_notar( self->bls_sign_fn, self->bls_sign_ctx, public_key, slot, hash, 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 } ); @@ -446,12 +479,14 @@ try_skip_window( ag_votor_t * self, ulong window_start = ag_first_slot_in_window( slot ); for( ulong s=window_start; svoted = 1; state->bad_window = 1; - ag_vote_t vote = ag_vote_construct_skip( self->bls_sign_fn, self->bls_sign_ctx, s, own_rank( self, s ), self->shred_version ); + ag_vote_t vote = ag_vote_construct_skip( self->bls_sign_fn, self->bls_sign_ctx, public_key, s, 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 } ); } @@ -562,8 +597,13 @@ 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 * public_key = own_bls_pubkey( self, slot ); + if( FD_UNLIKELY( !public_key ) ) { + try_skip_window( self, slot ); + break; + } - ag_vote_t vote = ag_vote_construct_notar_fallback( self->bls_sign_fn, self->bls_sign_ctx, slot, hash, own_rank( self, slot ), self->shred_version ); + ag_vote_t vote = ag_vote_construct_notar_fallback( self->bls_sign_fn, self->bls_sign_ctx, public_key, slot, hash, 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 ); @@ -573,8 +613,13 @@ ag_votor_handle_pool_event( ag_votor_t * self, case AG_EVENT_POOL_SAFE_TO_SKIP: { ulong slot = event->safe_to_skip; + uchar const * public_key = own_bls_pubkey( self, slot ); + if( FD_UNLIKELY( !public_key ) ) { + try_skip_window( self, slot ); + break; + } - ag_vote_t vote = ag_vote_construct_skip_fallback( self->bls_sign_fn, self->bls_sign_ctx, slot, own_rank( self, slot ), self->shred_version ); + ag_vote_t vote = ag_vote_construct_skip_fallback( self->bls_sign_fn, self->bls_sign_ctx, public_key, slot, 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 ); diff --git a/src/choreo/votor/ag_votor.h b/src/choreo/votor/ag_votor.h index 2ea50592357..710c7e1146e 100644 --- a/src/choreo/votor/ag_votor.h +++ b/src/choreo/votor/ag_votor.h @@ -25,6 +25,14 @@ ag_votor_advance_epoch( ag_votor_t * self, ulong epoch_rank, ulong epoch_slot ); +/* Sets the BLS public key selector for the most recently advanced + epoch. If this is not called after advancing an epoch, the votor + does not vote in that epoch. */ + +void +ag_votor_set_bls_pubkey( ag_votor_t * self, + uchar const public_key[ static FD_BLS_PUB_COMPRESSED_SZ ] ); + void ag_votor_set_shred_version( ag_votor_t * self, ushort shred_version ); diff --git a/src/choreo/votor/test_ag_cert_builder.h b/src/choreo/votor/test_ag_cert_builder.h index c69af4104c5..e3453b4470f 100644 --- a/src/choreo/votor/test_ag_cert_builder.h +++ b/src/choreo/votor/test_ag_cert_builder.h @@ -10,17 +10,74 @@ #include "ag_epoch_info.h" #include "ag_vote.h" +static uchar const test_bls_public_key[ FD_BLS_PUB_COMPRESSED_SZ ] = {0}; + /* sec_sign_fn is the fd_bls_sign_fn of a test that holds the secret key in memory; ctx points to the fd_bls_sec_t. */ static void sec_sign_fn( void * ctx, fd_bls_sig_t * sig, + uchar const * public_key, uchar const * msg, ulong msg_sz ) { + (void)public_key; fd_bls_sec_sign( (fd_bls_sec_t const *)ctx, msg, msg_sz, sig ); } +static inline ag_vote_t +test_ag_vote_construct_notar( fd_bls_sign_fn sign_fn, + void * sign_ctx, + ulong slot, + ag_block_hash_t const hash, + ushort rank, + ushort shred_version ) { + return ag_vote_construct_notar( sign_fn, sign_ctx, test_bls_public_key, slot, hash, rank, shred_version ); +} + +static inline ag_vote_t +test_ag_vote_construct_final( fd_bls_sign_fn sign_fn, + void * sign_ctx, + ulong slot, + ushort rank, + ushort shred_version ) { + return ag_vote_construct_final( sign_fn, sign_ctx, test_bls_public_key, slot, rank, shred_version ); +} + +static inline ag_vote_t +test_ag_vote_construct_skip( fd_bls_sign_fn sign_fn, + void * sign_ctx, + ulong slot, + ushort rank, + ushort shred_version ) { + return ag_vote_construct_skip( sign_fn, sign_ctx, test_bls_public_key, slot, rank, shred_version ); +} + +static inline ag_vote_t +test_ag_vote_construct_notar_fallback( fd_bls_sign_fn sign_fn, + void * sign_ctx, + ulong slot, + ag_block_hash_t const hash, + ushort rank, + ushort shred_version ) { + return ag_vote_construct_notar_fallback( sign_fn, sign_ctx, test_bls_public_key, slot, hash, rank, shred_version ); +} + +static inline ag_vote_t +test_ag_vote_construct_skip_fallback( fd_bls_sign_fn sign_fn, + void * sign_ctx, + ulong slot, + ushort rank, + ushort shred_version ) { + return ag_vote_construct_skip_fallback( sign_fn, sign_ctx, test_bls_public_key, slot, rank, shred_version ); +} + +#define ag_vote_construct_notar test_ag_vote_construct_notar +#define ag_vote_construct_final test_ag_vote_construct_final +#define ag_vote_construct_skip test_ag_vote_construct_skip +#define ag_vote_construct_notar_fallback test_ag_vote_construct_notar_fallback +#define ag_vote_construct_skip_fallback test_ag_vote_construct_skip_fallback + static inline void agg_add( fd_bls_agg_t * agg, ulong rank, diff --git a/src/choreo/votor/test_ag_vote.c b/src/choreo/votor/test_ag_vote.c index ee98cf1e75e..a886a0c3476 100644 --- a/src/choreo/votor/test_ag_vote.c +++ b/src/choreo/votor/test_ag_vote.c @@ -8,8 +8,10 @@ static void sec_sign_fn( void * ctx, fd_bls_sig_t * sig, + uchar const * public_key, uchar const * msg, ulong msg_sz ) { + (void)public_key; fd_bls_sec_sign( (fd_bls_sec_t const *)ctx, msg, msg_sz, sig ); } @@ -30,30 +32,32 @@ block_hash( ag_vote_t const * self ) { static void test_basic( void ) { fd_bls_sec_t sk; fd_memset( &sk, 9, FD_BLS_SEC_SZ ); + fd_bls_pub_t pk; fd_bls_sec_to_pub( &sk, &pk ); + uchar selector[ FD_BLS_PUB_COMPRESSED_SZ ]; blst_p1_compress( selector, &pk ); ag_block_hash_t h; memset( h, 0, sizeof(ag_block_hash_t) ); ag_vote_t v; - v = ag_vote_construct_notar( sec_sign_fn, &sk, 0UL, h, 0UL, TEST_SHRED_VERSION ); + v = ag_vote_construct_notar( sec_sign_fn, &sk, selector, 0UL, h, 0UL, TEST_SHRED_VERSION ); { char cstr[ AG_VOTE_CSTR_MAX ]; FD_TEST( !strncmp( ag_vote_to_cstr( &v, cstr ), "Notar { slot: 0, hash: 000000...", 32UL ) ); FD_LOG_NOTICE(( "%s", cstr )); } FD_TEST( v.kind==AG_VOTE_KIND_NOTAR ); FD_TEST( ag_vote_slot( &v )==0UL ); FD_TEST( ag_vote_rank( &v )==0UL ); FD_TEST( !memcmp( v.notar.block_hash, h, sizeof(ag_block_hash_t) ) ); - v = ag_vote_construct_notar_fallback( sec_sign_fn, &sk, 1UL, h, 2UL, TEST_SHRED_VERSION ); + v = ag_vote_construct_notar_fallback( sec_sign_fn, &sk, selector, 1UL, h, 2UL, TEST_SHRED_VERSION ); FD_TEST( v.kind==AG_VOTE_KIND_NOTAR_FALLBACK ); FD_TEST( !memcmp( v.notar_fallback.block_hash, h, sizeof(ag_block_hash_t) ) ); - v = ag_vote_construct_skip( sec_sign_fn, &sk, 3UL, 0UL, TEST_SHRED_VERSION ); + v = ag_vote_construct_skip( sec_sign_fn, &sk, selector, 3UL, 0UL, TEST_SHRED_VERSION ); FD_TEST( v.kind==AG_VOTE_KIND_SKIP ); FD_TEST( block_hash( &v )==NULL ); - v = ag_vote_construct_skip_fallback( sec_sign_fn, &sk, 3UL, 0UL, TEST_SHRED_VERSION ); + v = ag_vote_construct_skip_fallback( sec_sign_fn, &sk, selector, 3UL, 0UL, TEST_SHRED_VERSION ); FD_TEST( v.kind==AG_VOTE_KIND_SKIP_FALLBACK ); FD_TEST( block_hash( &v )==NULL ); - v = ag_vote_construct_final( sec_sign_fn, &sk, 4UL, 0UL, TEST_SHRED_VERSION ); + v = ag_vote_construct_final( sec_sign_fn, &sk, selector, 4UL, 0UL, TEST_SHRED_VERSION ); FD_TEST( v.kind==AG_VOTE_KIND_FINAL ); FD_TEST( block_hash( &v )==NULL ); FD_TEST( ag_vote_slot( &v )==4UL ); @@ -133,14 +137,15 @@ test_serialize( void ) { uchar ikm[ 64 ]; for( ulong i=0UL; i<64UL; i++ ) ikm[i] = (uchar)(i+1u); fd_bls_sec_t sk; fd_bls_sec_derive( &sk, ikm, sizeof(ikm) ); fd_bls_pub_t pk; fd_bls_sec_to_pub( &sk, &pk ); + uchar selector[ FD_BLS_PUB_COMPRESSED_SZ ]; blst_p1_compress( selector, &pk ); ag_block_hash_t h; for( ulong i=0UL; i<32UL; i++ ) h[i] = (uchar)(0xA0u+i); ag_vote_t v; - v = ag_vote_construct_notar( sec_sign_fn, &sk, 12345UL, h, 7UL, TEST_SHRED_VERSION ); check_wire( &v, &pk ); - v = ag_vote_construct_final( sec_sign_fn, &sk, 7UL, 1UL, TEST_SHRED_VERSION ); check_wire( &v, &pk ); - v = ag_vote_construct_skip( sec_sign_fn, &sk, 42UL, 3UL, TEST_SHRED_VERSION ); check_wire( &v, &pk ); - v = ag_vote_construct_notar_fallback( sec_sign_fn, &sk, 99UL, h, 65535UL, TEST_SHRED_VERSION ); check_wire( &v, &pk ); - v = ag_vote_construct_skip_fallback( sec_sign_fn, &sk, 42UL, 3UL, TEST_SHRED_VERSION ); check_wire( &v, &pk ); + v = ag_vote_construct_notar( sec_sign_fn, &sk, selector, 12345UL, h, 7UL, TEST_SHRED_VERSION ); check_wire( &v, &pk ); + v = ag_vote_construct_final( sec_sign_fn, &sk, selector, 7UL, 1UL, TEST_SHRED_VERSION ); check_wire( &v, &pk ); + v = ag_vote_construct_skip( sec_sign_fn, &sk, selector, 42UL, 3UL, TEST_SHRED_VERSION ); check_wire( &v, &pk ); + v = ag_vote_construct_notar_fallback( sec_sign_fn, &sk, selector, 99UL, h, 65535UL, TEST_SHRED_VERSION ); check_wire( &v, &pk ); + v = ag_vote_construct_skip_fallback( sec_sign_fn, &sk, selector, 42UL, 3UL, TEST_SHRED_VERSION ); check_wire( &v, &pk ); FD_LOG_NOTICE(( "vote serialize round trip pass" )); } diff --git a/src/choreo/votor/test_ag_votor.c b/src/choreo/votor/test_ag_votor.c index dfb171ffe7d..f76a59ee8e9 100644 --- a/src/choreo/votor/test_ag_votor.c +++ b/src/choreo/votor/test_ag_votor.c @@ -19,8 +19,27 @@ static uchar scratch[ SCRATCH_MAX ] __attribute__((aligned(128))); static fd_bls_sec_t g_sk [ NV ]; +static uchar g_bls_selector[ NV ][ FD_BLS_PUB_COMPRESSED_SZ ]; static ag_validator_info_t g_info[ NV ]; static ulong g_hash_ctr = 0UL; +static uchar g_last_bls_selector[ FD_BLS_PUB_COMPRESSED_SZ ]; + +static void +capture_sign_fn( void * ctx, + fd_bls_sig_t * sig, + uchar const * public_key, + uchar const * msg, + ulong msg_sz ) { + (void)ctx; + memcpy( g_last_bls_selector, public_key, FD_BLS_PUB_COMPRESSED_SZ ); + for( ulong i=0UL; isecret_key, ikm, sizeof(ikm) ); + fd_memzero_explicit( ikm, sizeof(ikm) ); + + fd_bls_pub_t public_key[1]; + fd_bls_sec_to_pub( &key->secret_key, public_key ); + blst_p1_compress( key->public_key, public_key ); +} + +fd_keyguard_bls_key_t const * +fd_keyguard_bls_key_query( fd_keyguard_bls_key_t const * keys, + ulong key_cnt, + uchar const public_key[ static FD_KEYGUARD_BLS_PUBKEY_SZ ] ) { + for( ulong i=0UL; isecret_key, + request+FD_KEYGUARD_BLS_PUBKEY_SZ, + request_sz-FD_KEYGUARD_BLS_PUBKEY_SZ, + signature ); + return 1; +} diff --git a/src/disco/keyguard/fd_keyguard_bls.h b/src/disco/keyguard/fd_keyguard_bls.h new file mode 100644 index 00000000000..8f326277329 --- /dev/null +++ b/src/disco/keyguard/fd_keyguard_bls.h @@ -0,0 +1,49 @@ +#ifndef HEADER_fd_src_disco_keyguard_fd_keyguard_bls_h +#define HEADER_fd_src_disco_keyguard_fd_keyguard_bls_h + +#include "fd_keyguard.h" +#include "../../ballet/bls/fd_bls.h" +#include "../../ballet/ed25519/fd_ed25519.h" + +#define FD_KEYGUARD_BLS_KEY_MAX (17UL) /* identity plus 16 authorized voters */ + +FD_STATIC_ASSERT( FD_KEYGUARD_BLS_PUBKEY_SZ==FD_BLS_PUB_COMPRESSED_SZ, bls_public_key_size ); +FD_STATIC_ASSERT( FD_KEYGUARD_BLS_SIG_SZ ==FD_BLS_SIG_SZ, bls_signature_size ); + +struct fd_keyguard_bls_key { + fd_bls_sec_t secret_key; + uchar public_key[ FD_KEYGUARD_BLS_PUBKEY_SZ ]; /* canonical compressed encoding */ +}; +typedef struct fd_keyguard_bls_key fd_keyguard_bls_key_t; + +FD_STATIC_ASSERT( sizeof(fd_keyguard_bls_key_t)*FD_KEYGUARD_BLS_KEY_MAX<=4096UL, bls_keys_fit_protected_page ); + +FD_PROTOTYPES_BEGIN + +void FD_FN_SENSITIVE +fd_keyguard_bls_key_derive( fd_keyguard_bls_key_t * key, + uchar const ed25519_public_key[ static 32 ], + uchar const ed25519_private_key[ static 32 ], + fd_sha512_t * sha ); + +FD_FN_PURE fd_keyguard_bls_key_t const * +fd_keyguard_bls_key_query( fd_keyguard_bls_key_t const * keys, + ulong key_cnt, + uchar const public_key[ static FD_KEYGUARD_BLS_PUBKEY_SZ ] ); + +ulong +fd_keyguard_bls_request_encode( uchar * request, + uchar const public_key[ static FD_KEYGUARD_BLS_PUBKEY_SZ ], + uchar const * payload, + ulong payload_sz ); + +int FD_FN_SENSITIVE +fd_keyguard_bls_sign_request( fd_keyguard_bls_key_t const * keys, + ulong key_cnt, + uchar const * request, + ulong request_sz, + fd_bls_sig_t * signature ); + +FD_PROTOTYPES_END + +#endif /* HEADER_fd_src_disco_keyguard_fd_keyguard_bls_h */ diff --git a/src/disco/keyguard/fd_keyguard_client.c b/src/disco/keyguard/fd_keyguard_client.c index 517a7d23766..8257fd5c573 100644 --- a/src/disco/keyguard/fd_keyguard_client.c +++ b/src/disco/keyguard/fd_keyguard_client.c @@ -1,5 +1,6 @@ #include "fd_keyguard_client.h" #include "fd_keyguard.h" +#include "fd_keyguard_bls.h" #include "../../tango/mcache/fd_mcache.h" #include "../../tango/dcache/fd_dcache.h" @@ -99,8 +100,20 @@ fd_keyguard_client_sign( fd_keyguard_client_t * client, uchar const * sign_data, ulong sign_data_len, int sign_type ) { - ulong signature_sz = fd_ulong_if( sign_type==FD_KEYGUARD_SIGN_TYPE_BLS, FD_KEYGUARD_BLS_SIG_SZ, 64UL ); - fd_keyguard_client_sign_sz( client, signature, signature_sz, sign_data, sign_data_len, sign_type ); + FD_TEST( sign_type!=FD_KEYGUARD_SIGN_TYPE_BLS ); + fd_keyguard_client_sign_sz( client, signature, 64UL, sign_data, sign_data_len, sign_type ); +} + +void +fd_keyguard_client_bls_sign( fd_keyguard_client_t * client, + uchar * signature, + uchar const * public_key, + uchar const * sign_data, + ulong sign_data_len ) { + FD_TEST( sign_data_len<=FD_KEYGUARD_SIGN_REQ_MTU-FD_KEYGUARD_BLS_PUBKEY_SZ ); + uchar request[ FD_KEYGUARD_SIGN_REQ_MTU ]; + ulong request_sz = fd_keyguard_bls_request_encode( request, public_key, sign_data, sign_data_len ); + fd_keyguard_client_sign_sz( client, signature, FD_KEYGUARD_BLS_SIG_SZ, request, request_sz, FD_KEYGUARD_SIGN_TYPE_BLS ); } void diff --git a/src/disco/keyguard/fd_keyguard_client.h b/src/disco/keyguard/fd_keyguard_client.h index d1cbbd8cdeb..21a1223be01 100644 --- a/src/disco/keyguard/fd_keyguard_client.h +++ b/src/disco/keyguard/fd_keyguard_client.h @@ -85,10 +85,11 @@ fd_keyguard_client_delete( void * shclient ) { return shclient; } will abort the whole program with a critical error. The response is written into the signature buffer, which must be at - least that large: FD_KEYGUARD_BLS_SIG_SZ (192) bytes for - FD_KEYGUARD_SIGN_TYPE_BLS, 64 bytes for every other type. + least 64 bytes. - sign_type is in FD_KEYGUARD_SIGN_TYPE_{...}. */ + sign_type is in FD_KEYGUARD_SIGN_TYPE_{...} and must not be + FD_KEYGUARD_SIGN_TYPE_BLS. Use fd_keyguard_client_bls_sign for BLS + requests. */ void fd_keyguard_client_sign( fd_keyguard_client_t * client, @@ -97,6 +98,19 @@ fd_keyguard_client_sign( fd_keyguard_client_t * client, ulong sign_data_len, int sign_type ); +/* fd_keyguard_client_bls_sign requests a BLS signature over + [sign_data,sign_data+sign_data_len). public_key selects the + keyguard-owned BLS secret key and is sent alongside, but is not part + of the signed message. signature must have room for + FD_KEYGUARD_BLS_SIG_SZ bytes. */ + +void +fd_keyguard_client_bls_sign( fd_keyguard_client_t * client, + uchar * signature, + uchar const * public_key, + uchar const * sign_data, + ulong sign_data_len ); + /* fd_keyguard_client_vote_txn_sign sends a remote signing request to the signing server, and blocks (spins) until the response is received. diff --git a/src/disco/keyguard/fd_keyguard_match.c b/src/disco/keyguard/fd_keyguard_match.c index 72ec959f69f..001e8396396 100644 --- a/src/disco/keyguard/fd_keyguard_match.c +++ b/src/disco/keyguard/fd_keyguard_match.c @@ -337,19 +337,20 @@ fd_keyguard_payload_matches_ag_vote( uchar const * data, ulong sz, int sign_type ) { - /* Alpenglow vote payload produced by ag_vote_signing_ser: + /* Alpenglow BLS request: + [48 bytes BLS public key selector, not signed] u8 tag (1..5, WireConsensusMessageKind vote tags) u64 slot [32 bytes block id] only for notar (1) and notar fallback (4) u16 shred_version */ if( sign_type != FD_KEYGUARD_SIGN_TYPE_BLS ) return 0; - if( sz!=11UL && sz!=43UL ) return 0; - uchar tag = data[ 0 ]; + if( sz!=FD_KEYGUARD_BLS_PUBKEY_SZ+11UL && sz!=FD_KEYGUARD_BLS_PUBKEY_SZ+43UL ) return 0; + uchar tag = data[ FD_KEYGUARD_BLS_PUBKEY_SZ ]; if( tag<1 || tag>5 ) return 0; int has_hash = ( tag==1 ) | ( tag==4 ); - return has_hash ? ( sz==43UL ) : ( sz==11UL ); + return has_hash ? ( sz==FD_KEYGUARD_BLS_PUBKEY_SZ+43UL ) : ( sz==FD_KEYGUARD_BLS_PUBKEY_SZ+11UL ); } FD_FN_PURE int diff --git a/src/disco/keyguard/fd_sign_tile.c b/src/disco/keyguard/fd_sign_tile.c index 2a9da861951..fbbbc8da428 100644 --- a/src/disco/keyguard/fd_sign_tile.c +++ b/src/disco/keyguard/fd_sign_tile.c @@ -4,6 +4,7 @@ #include "generated/fd_sign_tile_seccomp.h" #include "../keyguard/fd_keyguard.h" +#include "../keyguard/fd_keyguard_bls.h" #include "../keyguard/fd_keyload.h" #include "../keyguard/fd_keyswitch.h" #include "../../discof/admin/fd_adminctl.h" @@ -57,7 +58,7 @@ typedef struct { uchar * public_key; uchar * private_key; - uchar * bls_private_key; /* alpenglow BLS voting key */ + fd_keyguard_bls_key_t * bls_keys; uchar tip_payment_program [32]; uchar tip_distribution_program[32]; @@ -93,14 +94,7 @@ derive_fields( fd_sign_ctx_t * ctx ) { fd_base58_encode_32( ctx->public_key, &ctx->public_key_base58_sz, (char *)ctx->concat ); ctx->concat[ ctx->public_key_base58_sz ] = '-'; - /* Alpenglow BLS key derivation, matching - solana_bls_signatures::SecretKey::derive_from_signer: the BLS IKM - is the identity's ed25519 signature over a fixed message. */ - static char const derive_msg[] = "bls-key-derive-alpenglow"; - uchar ikm[ 64 ]; - fd_ed25519_sign( ikm, (uchar const *)derive_msg, sizeof(derive_msg)-1UL, ctx->public_key, ctx->private_key, ctx->sha512 ); - fd_bls_sec_derive( (fd_bls_sec_t *)fd_type_pun( ctx->bls_private_key ), ikm, sizeof(ikm) ); - fd_memzero_explicit( ikm, sizeof(ikm) ); + fd_keyguard_bls_key_derive( &ctx->bls_keys[ 0 ], ctx->public_key, ctx->private_key, ctx->sha512 ); } static void FD_FN_SENSITIVE @@ -142,10 +136,15 @@ during_housekeeping_sensitive( fd_sign_ctx_t * ctx ) { fd_memzero_explicit( ctx->av_keyswitch->bytes, 32UL ); FD_COMPILER_MFENCE(); memcpy( ctx->authorized_voter_pubkeys[ ctx->authorized_voters_cnt ], ctx->av_keyswitch->bytes + 32UL, 32UL ); + fd_keyguard_bls_key_derive( &ctx->bls_keys[ ctx->authorized_voters_cnt+1UL ], + ctx->authorized_voter_pubkeys[ ctx->authorized_voters_cnt ], + ctx->authorized_voter_private_keys[ ctx->authorized_voters_cnt ], + ctx->sha512 ); ctx->authorized_voters_cnt++; } else if( FD_LIKELY( param==FD_KEYSWITCH_PARAM_AV_CLEAR ) ) { fd_memzero_explicit( ctx->authorized_voter_private_keys, sizeof( ctx->authorized_voter_private_keys ) ); fd_memzero_explicit( ctx->authorized_voter_pubkeys, sizeof( ctx->authorized_voter_pubkeys ) ); + fd_memzero_explicit( ctx->bls_keys+1, sizeof(fd_keyguard_bls_key_t)*(FD_KEYGUARD_BLS_KEY_MAX-1UL) ); ctx->authorized_voters_cnt = 0UL; } else { FD_LOG_CRIT(( "keyswitch: unexpected authorized voter operation %lu", param )); @@ -275,7 +274,12 @@ after_frag_sensitive( void * _ctx, } case FD_KEYGUARD_SIGN_TYPE_BLS: { fd_bls_sig_t bls_sig[1]; - fd_bls_sec_sign( (fd_bls_sec_t const *)fd_type_pun_const( ctx->bls_private_key ), ctx->_data, sz, bls_sig ); + if( FD_UNLIKELY( !fd_keyguard_bls_sign_request( ctx->bls_keys, + ctx->authorized_voters_cnt+1UL, + ctx->_data, + sz, + bls_sig ) ) ) + FD_LOG_EMERG(( "votor requested an unknown BLS public key" )); fd_bls_sig_ser( dst, bls_sig ); out_sz = FD_KEYGUARD_BLS_SIG_SZ; break; @@ -314,8 +318,9 @@ privileged_init_sensitive( fd_topo_t const * topo, ctx->private_key = identity_key; ctx->public_key = identity_key + 32UL; - ctx->bls_private_key = fd_keyload_alloc_protected_pages( 1UL, 2UL ); + ctx->bls_keys = fd_keyload_alloc_protected_pages( 1UL, 2UL ); + FD_TEST( tile->sign.authorized_voter_paths_cntauthorized_voters_cnt = tile->sign.authorized_voter_paths_cnt; for( ulong i=0UL; isign.authorized_voter_paths_cnt; i++ ) { uchar const * authorized_voter_key = fd_keyload_load( tile->sign.authorized_voter_paths[ i ], /* pubkey only: */ 0 ); @@ -364,6 +369,12 @@ unprivileged_init_sensitive( fd_topo_t const * topo, ctx->keyswitch = fd_keyswitch_join( fd_topo_obj_laddr( topo, tile->id_keyswitch_obj_id ) ); derive_fields( ctx ); + for( ulong i=0UL; iauthorized_voters_cnt; i++ ) { + fd_keyguard_bls_key_derive( &ctx->bls_keys[ i+1UL ], + ctx->authorized_voter_pubkeys[ i ], + ctx->authorized_voter_private_keys[ i ], + ctx->sha512 ); + } if( FD_LIKELY( tile->av_keyswitch_obj_id!=ULONG_MAX ) ) { ctx->av_keyswitch = fd_keyswitch_join( fd_topo_obj_laddr( topo, tile->av_keyswitch_obj_id ) ); diff --git a/src/disco/keyguard/test_keyguard.c b/src/disco/keyguard/test_keyguard.c index 77cc2af168c..6b915bc3e1a 100644 --- a/src/disco/keyguard/test_keyguard.c +++ b/src/disco/keyguard/test_keyguard.c @@ -1,4 +1,5 @@ #include "fd_keyguard.h" +#include "fd_keyguard_bls.h" #include "../../ballet/txn/fd_txn.h" static uchar v1_buf [ FD_TXN_MTU ]; @@ -156,8 +157,10 @@ test_vote_txn_oob( void ) { static void test_ag_vote_authorize( void ) { fd_keyguard_authority_t authority = {0}; - uchar skip[ 11 ] = { 3 /* skip */ }; - uchar notar[ 43 ] = { 1 /* notar */ }; + uchar skip [ FD_KEYGUARD_BLS_PUBKEY_SZ+11UL ] = {0}; + uchar notar[ FD_KEYGUARD_BLS_PUBKEY_SZ+43UL ] = {0}; + skip [ FD_KEYGUARD_BLS_PUBKEY_SZ ] = 3; /* skip */ + notar[ FD_KEYGUARD_BLS_PUBKEY_SZ ] = 1; /* notar */ FD_TEST( fd_keyguard_payload_authorize( &authority, skip, sizeof(skip), FD_KEYGUARD_ROLE_VOTOR, FD_KEYGUARD_SIGN_TYPE_BLS ) ); FD_TEST( fd_keyguard_payload_authorize( &authority, notar, sizeof(notar), FD_KEYGUARD_ROLE_VOTOR, FD_KEYGUARD_SIGN_TYPE_BLS ) ); @@ -169,13 +172,98 @@ test_ag_vote_authorize( void ) { /* hash-carrying tag with the short size and vice versa */ FD_TEST( !fd_keyguard_payload_authorize( &authority, notar, sizeof(skip), FD_KEYGUARD_ROLE_VOTOR, FD_KEYGUARD_SIGN_TYPE_BLS ) ); FD_TEST( !fd_keyguard_payload_authorize( &authority, skip, sizeof(notar), FD_KEYGUARD_ROLE_VOTOR, FD_KEYGUARD_SIGN_TYPE_BLS ) ); + /* selector is mandatory */ + FD_TEST( !fd_keyguard_payload_authorize( &authority, skip+FD_KEYGUARD_BLS_PUBKEY_SZ, 11UL, FD_KEYGUARD_ROLE_VOTOR, FD_KEYGUARD_SIGN_TYPE_BLS ) ); /* bad tag */ - skip[ 0 ] = 6; + skip[ FD_KEYGUARD_BLS_PUBKEY_SZ ] = 6; FD_TEST( !fd_keyguard_payload_authorize( &authority, skip, sizeof(skip), FD_KEYGUARD_ROLE_VOTOR, FD_KEYGUARD_SIGN_TYPE_BLS ) ); - skip[ 0 ] = 0; + skip[ FD_KEYGUARD_BLS_PUBKEY_SZ ] = 0; FD_TEST( !fd_keyguard_payload_authorize( &authority, skip, sizeof(skip), FD_KEYGUARD_ROLE_VOTOR, FD_KEYGUARD_SIGN_TYPE_BLS ) ); } +static void +test_bls_key_lookup( void ) { + fd_sha512_t sha[1]; + FD_TEST( fd_sha512_join( fd_sha512_new( sha ) ) ); + + /* Generated independently by Agave 4.1.0 `solana-keygen + bls_pubkey`. The keypair is private seed followed by Ed25519 + public key. */ + static uchar const agave_keypair[ 64 ] = { + 149, 138, 120, 246, 229, 211, 77, 206, 163, 78, 57, 172, 248, 93, 205, 236, + 20, 90, 0, 7, 157, 121, 25, 54, 212, 189, 91, 26, 164, 253, 110, 203, + 51, 129, 127, 165, 142, 4, 248, 195, 91, 93, 33, 19, 91, 130, 175, 9, + 229, 142, 180, 156, 23, 173, 190, 249, 207, 5, 181, 31, 251, 76, 2, 208 + }; + static uchar const expected_selector[ FD_KEYGUARD_BLS_PUBKEY_SZ ] = { + 149, 157, 254, 148, 170, 68, 3, 78, 50, 10, 2, 167, 49, 36, 104, 157, + 220, 152, 181, 228, 47, 146, 210, 186, 254, 247, 17, 30, 130, 234, 224, 119, + 213, 125, 40, 244, 22, 34, 81, 8, 254, 105, 239, 43, 186, 178, 113, 1 + }; + + fd_keyguard_bls_key_t keys[ 2 ]; + uchar other_private_key[ 32 ]; memset( other_private_key, 0x11, sizeof(other_private_key) ); + uchar other_public_key [ 32 ]; fd_ed25519_public_from_private( other_public_key, other_private_key, sha ); + fd_keyguard_bls_key_derive( &keys[ 0 ], other_public_key, other_private_key, sha ); + fd_keyguard_bls_key_derive( &keys[ 1 ], agave_keypair+32UL, agave_keypair, sha ); + FD_TEST( !memcmp( keys[ 1 ].public_key, expected_selector, sizeof(expected_selector) ) ); + + fd_bls_pub_t expected_public[1]; + FD_TEST( !fd_bls_pub_de( expected_public, expected_selector, sizeof(expected_selector) ) ); + + fd_keyguard_bls_key_t const * selected = + fd_keyguard_bls_key_query( keys, 2UL, expected_selector ); + FD_TEST( selected==&keys[ 1 ] ); + + uchar payload[ 11 ] = { 3 /* skip */ }; + fd_bls_sig_t sig[1]; + fd_bls_sec_sign( &selected->secret_key, payload, sizeof(payload), sig ); + FD_TEST( fd_bls_agg_verify( payload, sizeof(payload), expected_public, sig ) ); + + fd_bls_pub_t wrong_public[1]; + FD_TEST( !fd_bls_pub_de( wrong_public, keys[ 0 ].public_key, FD_KEYGUARD_BLS_PUBKEY_SZ ) ); + FD_TEST( !fd_bls_agg_verify( payload, sizeof(payload), wrong_public, sig ) ); + + uchar unknown_selector[ FD_KEYGUARD_BLS_PUBKEY_SZ ]; + memcpy( unknown_selector, expected_selector, sizeof(unknown_selector) ); + unknown_selector[ 0 ] ^= 1U; + FD_TEST( !fd_keyguard_bls_key_query( keys, 2UL, unknown_selector ) ); +} + +static void +test_bls_request_signing( void ) { + fd_sha512_t sha[1]; + FD_TEST( fd_sha512_join( fd_sha512_new( sha ) ) ); + uchar private_key[ 32 ]; memset( private_key, 0x33, sizeof(private_key) ); + uchar public_key [ 32 ]; fd_ed25519_public_from_private( public_key, private_key, sha ); + fd_keyguard_bls_key_t key[1]; + fd_keyguard_bls_key_derive( key, public_key, private_key, sha ); + + uchar payload[ 43 ]; + for( ulong i=0UL; ipublic_key, payload, sizeof(payload) ); + FD_TEST( request_sz==sizeof(request) ); + FD_TEST( !memcmp( request, key->public_key, FD_KEYGUARD_BLS_PUBKEY_SZ ) ); + FD_TEST( !memcmp( request+FD_KEYGUARD_BLS_PUBKEY_SZ, payload, sizeof(payload) ) ); + fd_keyguard_authority_t authority = {0}; + FD_TEST( fd_keyguard_payload_authorize( &authority, request, request_sz, FD_KEYGUARD_ROLE_VOTOR, FD_KEYGUARD_SIGN_TYPE_BLS ) ); + + fd_bls_sig_t sig[1]; + FD_TEST( fd_keyguard_bls_sign_request( key, 1UL, request, request_sz, sig ) ); + + fd_bls_pub_t bls_public_key[1]; + FD_TEST( !fd_bls_pub_de( bls_public_key, key->public_key, FD_KEYGUARD_BLS_PUBKEY_SZ ) ); + FD_TEST( fd_bls_agg_verify( payload, sizeof(payload), bls_public_key, sig ) ); + FD_TEST( !fd_bls_agg_verify( request, request_sz, bls_public_key, sig ) ); + + request[ 0 ] ^= 1U; + FD_TEST( !fd_keyguard_bls_sign_request( key, 1UL, request, request_sz, sig ) ); + FD_TEST( !fd_keyguard_bls_sign_request( key, 1UL, request, FD_KEYGUARD_BLS_PUBKEY_SZ-1UL, sig ) ); +} + int main( int argc, char ** argv ) { @@ -183,6 +271,8 @@ main( int argc, test_vote_txn_oob(); test_txn_v1_match(); test_ag_vote_authorize(); + test_bls_key_lookup(); + test_bls_request_signing(); FD_LOG_NOTICE(( "pass" )); return 0; } diff --git a/src/discof/votor/fd_votor_tile.c b/src/discof/votor/fd_votor_tile.c index 08dd3809a5f..31e9abcaf2a 100644 --- a/src/discof/votor/fd_votor_tile.c +++ b/src/discof/votor/fd_votor_tile.c @@ -346,11 +346,12 @@ FD_STATIC_ASSERT( FD_BLS_SIG_SZ==FD_KEYGUARD_BLS_SIG_SZ, bls_sig_sz ); static void sign_bls( void * signer_ctx, fd_bls_sig_t * sig, + uchar const * public_key, uchar const * payload, ulong payload_sz ) { fd_votor_tile_t * ctx = signer_ctx; uchar sig_bytes[ FD_BLS_SIG_SZ ]; - fd_keyguard_client_sign( ctx->keyguard_client, sig_bytes, payload, payload_sz, FD_KEYGUARD_SIGN_TYPE_BLS ); + fd_keyguard_client_bls_sign( ctx->keyguard_client, sig_bytes, public_key, payload, payload_sz ); if( FD_UNLIKELY( fd_bls_sig_de( sig, sig_bytes ) ) ) FD_LOG_CRIT(( "sign tile returned an invalid BLS signature" )); } @@ -695,6 +696,11 @@ handle_epoch( fd_votor_tile_t * ctx, ushort epoch_rank = fd_ushort_if( !!ctx->next_epoch_info, ctx->next_epoch_rank, ctx->curr_epoch_rank ); 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 public_key[ FD_BLS_PUB_COMPRESSED_SZ ]; + blst_p1_compress( public_key, &epoch_info->validators[ epoch_rank ].bls_key ); + ag_votor_set_bls_pubkey( ctx->votor, public_key ); + } /* update our leader schedule */