Line data Source code
1 : #include "fd_stake_ci.h"
2 : #include "../../util/net/fd_ip4.h" /* Just for debug */
3 :
4 : #define SORT_NAME sort_pubkey
5 3472326 : #define SORT_KEY_T fd_shred_dest_weighted_t
6 5510922 : #define SORT_BEFORE(a,b) (memcmp( (a).pubkey.uc, (b).pubkey.uc, 32UL )<0)
7 : #include "../../util/tmpl/fd_sort.c"
8 :
9 : /* We don't have or need real contact info for the local validator, but
10 : we want to be able to distinguish it from staked nodes with no
11 : contact info. */
12 147 : #define SELF_DUMMY_IP 1U
13 :
14 : void *
15 : fd_stake_ci_new( void * mem,
16 36 : fd_pubkey_t const * identity_key ) {
17 36 : fd_stake_ci_t * info = (fd_stake_ci_t *)mem;
18 :
19 36 : fd_stake_weight_t dummy_stakes[ 1 ] = {{ .key = {{0}}, .stake = 1UL }};
20 36 : fd_shred_dest_weighted_t dummy_dests[ 1 ] = {{ .pubkey = *identity_key, .ip4 = SELF_DUMMY_IP }};
21 :
22 : /* Initialize first 2 to satisfy invariants */
23 36 : info->stake_weight[ 0 ] = dummy_stakes[ 0 ];
24 36 : info->shred_dest [ 0 ] = dummy_dests [ 0 ];
25 108 : for( ulong i=0UL; i<2UL; i++ ) {
26 72 : fd_per_epoch_info_t * ei = info->epoch_info + i;
27 72 : ei->epoch = i;
28 72 : ei->start_slot = 0UL;
29 72 : ei->slot_cnt = 0UL;
30 72 : ei->excluded_stake = 0UL;
31 :
32 72 : ei->lsched = fd_epoch_leaders_join( fd_epoch_leaders_new( ei->_lsched, 0UL, 0UL, 1UL, 1UL, info->stake_weight, 0UL ) );
33 72 : ei->sdest = fd_shred_dest_join ( fd_shred_dest_new ( ei->_sdest, info->shred_dest, 1UL, ei->lsched, identity_key, 0UL ) );
34 72 : }
35 36 : info->identity_key[ 0 ] = *identity_key;
36 :
37 36 : return (void *)info;
38 36 : }
39 :
40 36 : fd_stake_ci_t * fd_stake_ci_join( void * mem ) { return (fd_stake_ci_t *)mem; }
41 :
42 33 : void * fd_stake_ci_leave ( fd_stake_ci_t * info ) { return (void *)info; }
43 33 : void * fd_stake_ci_delete( void * mem ) { return mem; }
44 :
45 :
46 : void
47 : fd_stake_ci_stake_msg_init( fd_stake_ci_t * info,
48 93 : uchar const * new_message ) {
49 93 : ulong const * hdr = fd_type_pun_const( new_message );
50 :
51 93 : ulong epoch = hdr[ 0 ];
52 93 : ulong staked_cnt = hdr[ 1 ];
53 93 : ulong start_slot = hdr[ 2 ];
54 93 : ulong slot_cnt = hdr[ 3 ];
55 93 : ulong excluded_stake = hdr[ 4 ];
56 :
57 93 : if( FD_UNLIKELY( staked_cnt > MAX_SHRED_DESTS ) )
58 0 : FD_LOG_ERR(( "The stakes -> Firedancer splice sent a malformed update with %lu stakes in it,"
59 93 : " but the maximum allowed is %lu", staked_cnt, MAX_SHRED_DESTS ));
60 :
61 93 : info->scratch->epoch = epoch;
62 93 : info->scratch->start_slot = start_slot;
63 93 : info->scratch->slot_cnt = slot_cnt;
64 93 : info->scratch->staked_cnt = staked_cnt;
65 93 : info->scratch->excluded_stake = excluded_stake;
66 :
67 93 : fd_memcpy( info->stake_weight, hdr+5UL, sizeof(fd_stake_weight_t)*staked_cnt );
68 93 : }
69 :
70 : static inline void
71 177 : log_summary( char const * msg, fd_stake_ci_t * info ) {
72 : #if 0
73 : fd_per_epoch_info_t const * ei = info->epoch_info;
74 : FD_LOG_NOTICE(( "Dumping stake contact information because %s", msg ));
75 : for( ulong i=0UL; i<2UL; i++ ) {
76 : FD_LOG_NOTICE(( " Dumping shred destination details for epoch %lu, slots [%lu, %lu)", ei[i].epoch, ei[i].start_slot, ei[i].start_slot+ei[i].slot_cnt ));
77 : fd_shred_dest_t * sdest = ei[i].sdest;
78 : for( fd_shred_dest_idx_t j=0; j<(fd_shred_dest_idx_t)fd_shred_dest_cnt_all( sdest ); j++ ) {
79 : fd_shred_dest_weighted_t * dest = fd_shred_dest_idx_to_dest( sdest, j );
80 : FD_LOG_NOTICE(( " %16lx %20lu " FD_IP4_ADDR_FMT " %hu ", *(ulong *)dest->pubkey.uc, dest->stake_lamports, FD_IP4_ADDR_FMT_ARGS( dest->ip4 ), dest->port ));
81 : }
82 : }
83 : #else
84 177 : (void)msg;
85 177 : (void)info;
86 177 : #endif
87 177 : }
88 :
89 : #define SET_NAME unhit_set
90 0 : #define SET_MAX MAX_SHRED_DESTS
91 : #include "../../util/tmpl/fd_set.c"
92 :
93 : void
94 90 : fd_stake_ci_stake_msg_fini( fd_stake_ci_t * info ) {
95 : /* The grossness here is a sign our abstractions are wrong and need to
96 : be fixed instead of just patched. We need to generate weighted
97 : shred destinations using a combination of the new stake information
98 : and whatever contact info we previously knew. */
99 90 : ulong epoch = info->scratch->epoch;
100 90 : ulong staked_cnt = info->scratch->staked_cnt;
101 :
102 : /* Just take the first one arbitrarily because they both have the same
103 : contact info, other than possibly some staked nodes with no contact
104 : info. */
105 90 : fd_shred_dest_t * existing_sdest = info->epoch_info->sdest;
106 90 : ulong existing_dest_cnt = fd_shred_dest_cnt_all( existing_sdest );
107 :
108 : /* Keep track of the destinations in existing_sdest that are not
109 : staked in this new epoch, i.e. the ones we don't hit in the loop
110 : below. */
111 90 : unhit_set_t _unhit[ unhit_set_word_cnt ];
112 : /* This memsets to 0, right before we memset to 1, and is probably
113 : unnecessary, but using it without joining seems like a hack. */
114 90 : unhit_set_t * unhit = unhit_set_join( unhit_set_new( _unhit ) );
115 90 : unhit_set_full( unhit );
116 :
117 482715 : for( ulong i=0UL; i<staked_cnt; i++ ) {
118 482625 : fd_shred_dest_idx_t old_idx = fd_shred_dest_pubkey_to_idx( existing_sdest, &(info->stake_weight[ i ].key) );
119 482625 : fd_shred_dest_weighted_t * in_prev = fd_shred_dest_idx_to_dest( existing_sdest, old_idx );
120 482625 : info->shred_dest[ i ] = *in_prev;
121 482625 : if( FD_UNLIKELY( old_idx==FD_SHRED_DEST_NO_DEST ) ) {
122 : /* We got the generic empty entry, so fixup the pubkey */
123 120711 : info->shred_dest[ i ].pubkey = info->stake_weight[ i ].key;
124 361914 : } else {
125 361914 : unhit_set_remove( unhit, old_idx );
126 361914 : }
127 482625 : info->shred_dest[ i ].stake_lamports = info->stake_weight[ i ].stake;
128 482625 : }
129 :
130 90 : int any_destaked = 0;
131 90 : ulong j = staked_cnt;
132 279 : for( ulong idx=unhit_set_iter_init( unhit ); (idx<existing_dest_cnt) & (!unhit_set_iter_done( idx )) & (j<MAX_SHRED_DESTS);
133 189 : idx=unhit_set_iter_next( unhit, idx ) ) {
134 189 : fd_shred_dest_weighted_t * in_prev = fd_shred_dest_idx_to_dest( existing_sdest, (fd_shred_dest_idx_t)idx );
135 189 : if( FD_LIKELY( in_prev->ip4 ) ) {
136 147 : info->shred_dest[ j ] = *in_prev;
137 147 : any_destaked |= (in_prev->stake_lamports > 0UL);
138 147 : info->shred_dest[ j ].stake_lamports = 0UL;
139 147 : j++;
140 147 : }
141 189 : }
142 :
143 90 : unhit_set_delete( unhit_set_leave( unhit ) );
144 :
145 90 : if( FD_UNLIKELY( any_destaked ) ) {
146 : /* The unstaked list might be a little out of order because the
147 : destinations that were previously staked will be at the start of
148 : the unstaked list, sorted by their previous stake, instead of
149 : where they should be. If there weren't any destaked, then the
150 : only unstaked nodes come from the previous list, which we know
151 : was in order, perhaps skipping some, which doesn't ruin the
152 : order. */
153 18 : sort_pubkey_inplace( info->shred_dest + staked_cnt, j - staked_cnt );
154 18 : }
155 :
156 : /* Now we have a plausible shred_dest list. */
157 :
158 : /* Clear the existing info */
159 90 : fd_per_epoch_info_t * new_ei = info->epoch_info + (epoch % 2UL);
160 90 : fd_shred_dest_delete ( fd_shred_dest_leave ( new_ei->sdest ) );
161 90 : fd_epoch_leaders_delete( fd_epoch_leaders_leave( new_ei->lsched ) );
162 :
163 : /* And create the new one */
164 90 : ulong excluded_stake = info->scratch->excluded_stake;
165 :
166 90 : new_ei->epoch = epoch;
167 90 : new_ei->start_slot = info->scratch->start_slot;
168 90 : new_ei->slot_cnt = info->scratch->slot_cnt;
169 90 : new_ei->excluded_stake = excluded_stake;
170 :
171 90 : new_ei->lsched = fd_epoch_leaders_join( fd_epoch_leaders_new( new_ei->_lsched, epoch, new_ei->start_slot, new_ei->slot_cnt,
172 90 : staked_cnt, info->stake_weight, excluded_stake ) );
173 90 : new_ei->sdest = fd_shred_dest_join ( fd_shred_dest_new ( new_ei->_sdest, info->shred_dest, j,
174 90 : new_ei->lsched, info->identity_key, excluded_stake ) );
175 90 : log_summary( "stake update", info );
176 90 : }
177 :
178 90 : fd_shred_dest_weighted_t * fd_stake_ci_dest_add_init( fd_stake_ci_t * info ) { return info->shred_dest; }
179 :
180 : static inline void
181 : fd_stake_ci_dest_add_fini_impl( fd_stake_ci_t * info,
182 : ulong cnt,
183 174 : fd_per_epoch_info_t * ei ) {
184 : /* Initially we start with one list containing S+U staked and unstaked
185 : destinations jumbled together. In order to update sdest, we need
186 : to convert the list to S' staked destinations (taken from the
187 : existing sdest, though possibly updated) followed by U unstaked
188 : destinations.
189 :
190 : It's possible to do this in place, but at a cost of additional
191 : complexity (similar to memcpy vs memmove). Rather than do that, we
192 : build the combined list in shred_dest_temp. */
193 :
194 174 : ulong found_unstaked_cnt = 0UL;
195 174 : int any_new_unstaked = 0;
196 :
197 174 : ulong const staked_cnt = fd_shred_dest_cnt_staked( ei->sdest );
198 174 : ulong j = staked_cnt;
199 :
200 3859698 : for( ulong i=0UL; i<cnt; i++ ) {
201 3859524 : fd_shred_dest_idx_t idx = fd_shred_dest_pubkey_to_idx( ei->sdest, &(info->shred_dest[ i ].pubkey) );
202 3859524 : fd_shred_dest_weighted_t * dest = fd_shred_dest_idx_to_dest( ei->sdest, idx );
203 3859524 : if( FD_UNLIKELY( (dest->stake_lamports==0UL)&(j<MAX_SHRED_DESTS) ) ) {
204 : /* Copy this destination to the unstaked part of the new list.
205 : This also handles the new unstaked case */
206 482760 : info->shred_dest_temp[ j ] = info->shred_dest[ i ];
207 482760 : info->shred_dest_temp[ j ].stake_lamports = 0UL;
208 482760 : j++;
209 482760 : }
210 :
211 3859524 : if( FD_LIKELY( idx!=FD_SHRED_DEST_NO_DEST ) ) {
212 3738693 : dest->ip4 = info->shred_dest[ i ].ip4;
213 3738693 : dest->port = info->shred_dest[ i ].port;
214 3738693 : }
215 :
216 3859524 : any_new_unstaked |= (idx==FD_SHRED_DEST_NO_DEST);
217 3859524 : found_unstaked_cnt += (ulong)((idx!=FD_SHRED_DEST_NO_DEST) & (dest->stake_lamports==0UL));
218 3859524 : }
219 :
220 174 : if( FD_LIKELY( !any_new_unstaked && found_unstaked_cnt==fd_shred_dest_cnt_unstaked( ei->sdest ) ) ) {
221 : /* Because any_new_unstaked==0, the set of unstaked nodes in this
222 : update is fully contained in the set of unstaked nodes in the
223 : sdest. Then additionally, because the sets are the same size,
224 : they must actually be equal. In this case, we've already updated
225 : the existing shred_dest_weighted with the newest contact info we
226 : have, so there's nothing else to do. */
227 48 : return;
228 48 : }
229 :
230 : /* Otherwise something more significant changed and we need to
231 : regenerate the sdest. At this point, elements [staked_cnt, j) now
232 : contain all the current unstaked destinations. */
233 :
234 : /* Copy staked nodes to [0, staked_cnt). We've already applied the
235 : updated contact info to these. */
236 1929831 : for( ulong i=0UL; i<staked_cnt; i++ )
237 1929705 : info->shred_dest_temp[ i ] = *fd_shred_dest_idx_to_dest( ei->sdest, (fd_shred_dest_idx_t)i );
238 :
239 : /* The staked nodes are sorted properly because we use the index from
240 : sdest. We need to sort the unstaked nodes by pubkey though. */
241 126 : sort_pubkey_inplace( info->shred_dest_temp + staked_cnt, j - staked_cnt );
242 :
243 126 : fd_shred_dest_delete( fd_shred_dest_leave( ei->sdest ) );
244 :
245 126 : ei->sdest = fd_shred_dest_join( fd_shred_dest_new( ei->_sdest, info->shred_dest_temp, j, ei->lsched, info->identity_key,
246 126 : ei->excluded_stake ) );
247 :
248 126 : if( FD_UNLIKELY( ei->sdest==NULL ) ) {
249 : /* Happens if the identity key is not present, which can only happen
250 : if the current validator's stake is not in the top 40,200. We
251 : could initialize ei->sdest to a dummy value, but having the wrong
252 : stake weights could lead to potentially slashable issues
253 : elsewhere (e.g. we might product a block when we're not actually
254 : leader). We're just going to terminate in this case. */
255 0 : FD_LOG_ERR(( "Too many validators have higher stake than this validator. Cannot continue." ));
256 0 : }
257 126 : }
258 :
259 :
260 : void
261 : fd_stake_ci_dest_add_fini( fd_stake_ci_t * info,
262 87 : ulong cnt ) {
263 : /* The Rust side uses tvu_peers which typically excludes the local
264 : validator. In some cases, after a set-identity, it might still
265 : include the local validator though. If it doesn't include it, we
266 : need to add the local validator back. */
267 87 : FD_TEST( cnt<MAX_SHRED_DESTS );
268 87 : ulong i=0UL;
269 1929762 : for(; i<cnt; i++ ) if( FD_UNLIKELY( 0==memcmp( info->shred_dest[ i ].pubkey.uc, info->identity_key, 32UL ) ) ) break;
270 :
271 87 : if( FD_LIKELY( i==cnt ) ) {
272 87 : fd_shred_dest_weighted_t self_dests = { .pubkey = info->identity_key[ 0 ], .ip4 = SELF_DUMMY_IP };
273 87 : info->shred_dest[ cnt++ ] = self_dests;
274 87 : } else {
275 0 : info->shred_dest[ i ].ip4 = SELF_DUMMY_IP;
276 0 : }
277 :
278 : /* Update both of them */
279 87 : fd_stake_ci_dest_add_fini_impl( info, cnt, info->epoch_info + 0UL );
280 87 : fd_stake_ci_dest_add_fini_impl( info, cnt, info->epoch_info + 1UL );
281 :
282 87 : log_summary( "dest update", info );
283 87 : }
284 :
285 :
286 : /* Returns a value in [0, 2) if found, and ULONG_MAX if not */
287 : static inline ulong
288 : fd_stake_ci_get_idx_for_slot( fd_stake_ci_t const * info,
289 1179 : ulong slot ) {
290 1179 : fd_per_epoch_info_t const * ei = info->epoch_info;
291 1179 : ulong idx = ULONG_MAX;
292 3537 : for( ulong i=0UL; i<2UL; i++ ) idx = fd_ulong_if( (ei[i].start_slot<=slot) & (slot-ei[i].start_slot<ei[i].slot_cnt), i, idx );
293 1179 : return idx;
294 1179 : }
295 :
296 :
297 : void
298 : fd_stake_ci_set_identity( fd_stake_ci_t * info,
299 12 : fd_pubkey_t const * identity_key ) {
300 : /* None of the stakes are changing, so we just need to regenerate the
301 : sdests, sightly adjusting the destination IP addresses. The only
302 : corner case is if the new identity is not present. */
303 36 : for( ulong i=0UL; i<2UL; i++ ) {
304 24 : fd_per_epoch_info_t * ei = info->epoch_info+i;
305 :
306 24 : fd_shred_dest_idx_t old_idx = fd_shred_dest_pubkey_to_idx( ei->sdest, info->identity_key );
307 24 : fd_shred_dest_idx_t new_idx = fd_shred_dest_pubkey_to_idx( ei->sdest, identity_key );
308 :
309 24 : FD_TEST( old_idx!=FD_SHRED_DEST_NO_DEST );
310 :
311 24 : if( FD_LIKELY( new_idx!=FD_SHRED_DEST_NO_DEST ) ) {
312 18 : fd_shred_dest_idx_to_dest( ei->sdest, old_idx )->ip4 = 0U;
313 18 : fd_shred_dest_idx_to_dest( ei->sdest, new_idx )->ip4 = SELF_DUMMY_IP;
314 :
315 18 : fd_shred_dest_update_source( ei->sdest, new_idx );
316 18 : } else {
317 6 : ulong staked_cnt = fd_shred_dest_cnt_staked ( ei->sdest );
318 6 : ulong unstaked_cnt = fd_shred_dest_cnt_unstaked( ei->sdest );
319 6 : if( FD_UNLIKELY( staked_cnt+unstaked_cnt==MAX_SHRED_DESTS ) ) {
320 0 : FD_LOG_ERR(( "too many validators in shred table to add a new validator with set-identity" ));
321 0 : }
322 : /* We'll add identity_key as a new unstaked validator. First copy
323 : all the staked ones, then place the new validator in the spot
324 : where it belongs according to lexicographic order. */
325 6 : ulong j=0UL;
326 24 : for(; j<staked_cnt; j++ ) info->shred_dest_temp[ j ] = *fd_shred_dest_idx_to_dest( ei->sdest, (fd_shred_dest_idx_t)j );
327 33 : for(; j<staked_cnt+unstaked_cnt; j++ ) {
328 33 : fd_shred_dest_weighted_t * wj = fd_shred_dest_idx_to_dest( ei->sdest, (fd_shred_dest_idx_t)j );
329 33 : if( FD_UNLIKELY( (memcmp( wj->pubkey.uc, identity_key->uc, 32UL )>=0) ) ) break;
330 27 : info->shred_dest_temp[ j ] = *wj;
331 27 : }
332 :
333 6 : info->shred_dest_temp[ j ].pubkey = *identity_key;
334 6 : info->shred_dest_temp[ j ].stake_lamports = 0UL;
335 6 : info->shred_dest_temp[ j ].ip4 = SELF_DUMMY_IP;
336 :
337 12 : for(; j<staked_cnt+unstaked_cnt; j++ ) info->shred_dest_temp[ j+1UL ] = *fd_shred_dest_idx_to_dest( ei->sdest, (fd_shred_dest_idx_t)j );
338 :
339 6 : fd_shred_dest_delete( fd_shred_dest_leave( ei->sdest ) );
340 :
341 6 : ei->sdest = fd_shred_dest_join( fd_shred_dest_new( ei->_sdest, info->shred_dest_temp, j+1UL, ei->lsched, identity_key,
342 6 : ei->excluded_stake ) );
343 6 : FD_TEST( ei->sdest );
344 6 : }
345 :
346 24 : }
347 12 : *info->identity_key = *identity_key;
348 12 : }
349 :
350 :
351 : fd_shred_dest_t *
352 : fd_stake_ci_get_sdest_for_slot( fd_stake_ci_t const * info,
353 591 : ulong slot ) {
354 591 : ulong idx = fd_stake_ci_get_idx_for_slot( info, slot );
355 591 : return idx!=ULONG_MAX ? info->epoch_info[ idx ].sdest : NULL;
356 591 : }
357 :
358 : fd_epoch_leaders_t *
359 : fd_stake_ci_get_lsched_for_slot( fd_stake_ci_t const * info,
360 588 : ulong slot ) {
361 588 : ulong idx = fd_stake_ci_get_idx_for_slot( info, slot );
362 588 : return idx!=ULONG_MAX ? info->epoch_info[ idx ].lsched : NULL;
363 588 : }
|