Line data Source code
1 : #ifndef HEADER_fd_src_flamenco_runtime_fd_alut_h
2 : #define HEADER_fd_src_flamenco_runtime_fd_alut_h
3 :
4 : /* fd_alut.h provides APIs for interpreting Solana address lookup table
5 : usages.
6 :
7 : https://solana.com/de/developers/guides/advanced/lookup-tables */
8 :
9 : #include "../../ballet/txn/fd_txn.h"
10 : #include "../../ballet/base58/fd_base58.h"
11 : #include "fd_runtime_err.h"
12 : #include "fd_system_ids.h"
13 : #include "sysvar/fd_sysvar_base.h"
14 :
15 168 : #define FD_ADDRLUT_STATUS_ACTIVATED (0)
16 108 : #define FD_ADDRLUT_STATUS_DEACTIVATING (1)
17 6 : #define FD_ADDRLUT_STATUS_DEACTIVATED (2)
18 :
19 : /* https://github.com/anza-xyz/agave/blob/368ea563c423b0a85cc317891187e15c9a321521/sdk/program/src/address_lookup_table/state.rs#L19 */
20 468 : #define FD_LOOKUP_TABLE_META_SIZE (56)
21 : #define FD_ADDRLUT_MAX_ENTRIES FD_SYSVAR_SLOT_HASHES_CAP
22 :
23 : /* Discriminants for the ProgramState enum wrapping the lookup table
24 : metadata in the on-disk account format. */
25 :
26 138 : #define FD_ALUT_STATE_DISC_UNINITIALIZED (0U)
27 222 : #define FD_ALUT_STATE_DISC_LOOKUP_TABLE (1U)
28 :
29 : /* fd_alut_meta_t is the in-memory representation of address lookup
30 : table account state (ProgramState + LookupTableMeta in Agave).
31 : The on-disk format is 56 bytes: u32 discriminant,
32 : u64 deactivation_slot, u64 last_extended_slot,
33 : u8 last_extended_slot_start_index, Option<Pubkey> authority
34 : (1-byte tag + 32 bytes), u16 _padding. When discriminant is
35 : FD_ALUT_STATE_DISC_UNINITIALIZED the remaining fields are ignored. */
36 :
37 : struct fd_alut_meta {
38 : uint discriminant;
39 : ulong deactivation_slot;
40 : ulong last_extended_slot;
41 : uchar last_extended_slot_start_index;
42 : uchar has_authority;
43 : fd_pubkey_t authority;
44 : };
45 : typedef struct fd_alut_meta fd_alut_meta_t;
46 :
47 : /* fd_alut_interp_t interprets indirect account references of a txn. */
48 :
49 : struct fd_alut_interp {
50 : fd_acct_addr_t * out_accts_alt;
51 : fd_txn_t const * txn;
52 : uchar const * txn_payload;
53 : fd_slot_hashes_t const * hashes;
54 : ulong slot;
55 : ulong alut_idx;
56 : ulong ro_indir_cnt;
57 : ulong rw_indir_cnt;
58 : };
59 :
60 : typedef struct fd_alut_interp fd_alut_interp_t;
61 :
62 : FD_PROTOTYPES_BEGIN
63 :
64 : /* fd_alut_state_encode writes a 56-byte ALUT state header into
65 : [buf, buf+bufsz). Uses meta->discriminant to decide the variant;
66 : fields other than discriminant are ignored when discriminant ==
67 : FD_ALUT_STATE_DISC_UNINITIALIZED. Returns 0 on success, -1 on short
68 : buffer. */
69 :
70 : static inline int
71 : fd_alut_state_encode( fd_alut_meta_t const * meta,
72 : uchar * buf,
73 111 : ulong bufsz ) {
74 111 : uchar * const _payload = buf;
75 111 : ulong const _payload_sz = bufsz;
76 111 : ulong _i = 0UL;
77 :
78 669 : # define CHECK( cond ) { if( FD_UNLIKELY( !(cond) ) ) { return -1; } }
79 669 : # define CHECK_LEFT( n ) CHECK( (n)<=(_payload_sz-_i) )
80 669 : # define INC( n ) (_i += (ulong)(n))
81 114 : # define CURSOR (_payload+_i)
82 :
83 111 : CHECK_LEFT( 4UL ); FD_STORE( uint, CURSOR, meta->discriminant ); INC( 4UL );
84 :
85 111 : if( meta->discriminant==FD_ALUT_STATE_DISC_LOOKUP_TABLE ) {
86 111 : CHECK_LEFT( 8UL ); FD_STORE( ulong, CURSOR, meta->deactivation_slot ); INC( 8UL );
87 111 : CHECK_LEFT( 8UL ); FD_STORE( ulong, CURSOR, meta->last_extended_slot ); INC( 8UL );
88 111 : CHECK_LEFT( 1UL ); FD_STORE( uchar, CURSOR, meta->last_extended_slot_start_index ); INC( 1UL );
89 :
90 111 : CHECK_LEFT( 1UL ); FD_STORE( uchar, CURSOR, (uchar)!!meta->has_authority ); INC( 1UL );
91 :
92 111 : if( meta->has_authority ) {
93 3 : CHECK_LEFT( 32UL ); fd_memcpy( CURSOR, meta->authority.key, 32UL ); INC( 32UL );
94 3 : }
95 :
96 : /* u16 _padding (always 0 on the wire). */
97 111 : CHECK_LEFT( 2UL ); FD_STORE( ushort, CURSOR, (ushort)0 ); INC( 2UL );
98 111 : }
99 :
100 111 : fd_memset( CURSOR, 0, _payload_sz-_i ); /* zero-pad the rest of the buffer */
101 :
102 111 : # undef CHECK
103 111 : # undef CHECK_LEFT
104 111 : # undef INC
105 111 : # undef CURSOR
106 :
107 111 : return 0;
108 111 : }
109 :
110 : /* fd_alut_state_decode reads an ALUT state header from
111 : [data, data+data_sz). On success populates *out (including
112 : out->discriminant) and returns 0. Returns -1 on decode failure
113 : (short buffer or unknown discriminant). */
114 :
115 : static inline int
116 : fd_alut_state_decode( uchar const * data,
117 : ulong data_sz,
118 138 : fd_alut_meta_t * out ) {
119 138 : uchar const * _payload = data;
120 138 : ulong const _payload_sz = data_sz;
121 138 : ulong _i = 0UL;
122 :
123 1050 : # define CHECK( cond ) { if( FD_UNLIKELY( !(cond) ) ) { return -1; } }
124 783 : # define CHECK_LEFT( n ) CHECK( (n)<=(_payload_sz-_i) )
125 777 : # define INC( n ) (_i += (ulong)(n))
126 138 : # define CURSOR (_payload+_i)
127 :
128 138 : CHECK_LEFT( 4UL ); uint disc = FD_LOAD( uint, CURSOR ); INC( 4UL );
129 138 : out->discriminant = disc;
130 :
131 138 : if( disc==FD_ALUT_STATE_DISC_UNINITIALIZED ) return 0;
132 :
133 135 : CHECK( disc==FD_ALUT_STATE_DISC_LOOKUP_TABLE );
134 :
135 132 : CHECK_LEFT( 8UL ); out->deactivation_slot = FD_LOAD( ulong, CURSOR ); INC( 8UL );
136 132 : CHECK_LEFT( 8UL ); out->last_extended_slot = FD_LOAD( ulong, CURSOR ); INC( 8UL );
137 132 : CHECK_LEFT( 1UL ); out->last_extended_slot_start_index = FD_LOAD( uchar, CURSOR ); INC( 1UL );
138 :
139 : /* Option<Pubkey> tag: bincode rejects any byte outside {0,1}. */
140 132 : CHECK_LEFT( 1UL ); uchar has_auth = FD_LOAD( uchar, CURSOR ); INC( 1UL );
141 132 : CHECK( !(has_auth & (uchar)~1U) );
142 114 : out->has_authority = has_auth;
143 :
144 114 : if( has_auth ) {
145 3 : CHECK_LEFT( 32UL );
146 3 : fd_memcpy( out->authority.key, CURSOR, 32UL );
147 3 : INC( 32UL );
148 3 : }
149 :
150 : /* u16 _padding. Value is ignored but the 2 bytes must be present. */
151 114 : CHECK_LEFT( 2UL ); INC( 2UL );
152 :
153 108 : # undef CHECK
154 108 : # undef CHECK_LEFT
155 108 : # undef INC
156 108 : # undef CURSOR
157 :
158 108 : return 0;
159 114 : }
160 :
161 : /* fd_alut_slot_hashes_position, fd_alut_status, and fd_alut_is_active
162 : are all helper methods for determining the number of active addresses
163 : in an address lookup table account. */
164 :
165 :
166 : /* Logic here is copied from slice::binary_search_by() in Rust. While
167 : not fully optimized, it aims to achieve fuzzing conformance for both
168 : sorted and unsorted inputs. */
169 : FD_FN_UNUSED static ulong
170 : fd_alut_slot_hashes_position( fd_slot_hashes_t const * hashes,
171 12 : ulong slot ) {
172 12 : ulong size = hashes->cnt;
173 12 : if( FD_UNLIKELY( size==0UL ) ) return ULONG_MAX;
174 :
175 12 : ulong base = 0UL;
176 60 : while( size>1UL ) {
177 48 : ulong half = size / 2UL;
178 48 : ulong mid = base + half;
179 48 : ulong mid_slot = hashes->elems[ mid ].slot;
180 48 : base = (slot>mid_slot) ? base : mid;
181 48 : size -= half;
182 48 : }
183 :
184 12 : return hashes->elems[ base ].slot==slot ? base : ULONG_MAX;
185 12 : }
186 :
187 : /* https://github.com/anza-xyz/agave/blob/368ea563c423b0a85cc317891187e15c9a321521/sdk/program/src/address_lookup_table/state.rs#L81-L104 */
188 : FD_FN_UNUSED static uchar
189 : fd_alut_status( fd_alut_meta_t const * state,
190 : ulong current_slot,
191 99 : fd_slot_hashes_t const * slot_hashes ) {
192 99 : if( state->deactivation_slot==ULONG_MAX ) {
193 84 : return FD_ADDRLUT_STATUS_ACTIVATED;
194 84 : }
195 :
196 15 : if( state->deactivation_slot==current_slot ) {
197 3 : return FD_ADDRLUT_STATUS_DEACTIVATING;
198 3 : }
199 :
200 12 : ulong slot_hash_position = fd_alut_slot_hashes_position( slot_hashes, state->deactivation_slot );
201 12 : if( slot_hash_position!=ULONG_MAX ) {
202 9 : return FD_ADDRLUT_STATUS_DEACTIVATING;
203 9 : }
204 :
205 3 : return FD_ADDRLUT_STATUS_DEACTIVATED;
206 12 : }
207 :
208 : /* https://github.com/anza-xyz/agave/blob/368ea563c423b0a85cc317891187e15c9a321521/sdk/program/src/address_lookup_table/state.rs#L72-L78 */
209 : FD_FN_UNUSED static uchar
210 : fd_alut_is_active( fd_alut_meta_t const * self,
211 : ulong current_slot,
212 99 : fd_slot_hashes_t const * slot_hashes ) {
213 99 : uchar status = fd_alut_status( self, current_slot, slot_hashes );
214 99 : switch( status ) {
215 84 : case FD_ADDRLUT_STATUS_ACTIVATED:
216 96 : case FD_ADDRLUT_STATUS_DEACTIVATING:
217 96 : return 1;
218 3 : case FD_ADDRLUT_STATUS_DEACTIVATED:
219 3 : return 0;
220 0 : default:
221 0 : FD_LOG_CRIT(( "invalid lut status %d", status ));
222 99 : }
223 99 : }
224 :
225 : /* fd_alut_active_addresses_len returns the number of active addresses
226 : in an address lookup table account.
227 : https://github.com/anza-xyz/agave/blob/368ea563c423b0a85cc317891187e15c9a321521/sdk/program/src/address_lookup_table/state.rs#L142-L164 */
228 : FD_FN_UNUSED static int
229 : fd_alut_active_addresses_len( fd_alut_meta_t const * self,
230 : ulong current_slot,
231 : fd_slot_hashes_t const * slot_hashes,
232 : ulong addresses_len,
233 99 : ulong * active_addresses_len /* out */ ) {
234 99 : if( FD_UNLIKELY( !fd_alut_is_active( self, current_slot, slot_hashes ) ) ) {
235 3 : return FD_RUNTIME_TXN_ERR_ADDRESS_LOOKUP_TABLE_NOT_FOUND;
236 3 : }
237 :
238 96 : *active_addresses_len = ( current_slot > self->last_extended_slot )
239 96 : ? addresses_len
240 96 : : self->last_extended_slot_start_index;
241 :
242 96 : return FD_RUNTIME_EXECUTE_SUCCESS;
243 99 : }
244 :
245 : /* fd_alut_interp_new creates a new ALUT interpreter.
246 : Will write indirectly referenced addresses to out_addrs.
247 : txn_payload points to a valid serialized transaction, txn points to
248 : the associated transaction descriptor. alut_interp retains a write
249 : interest in out_addrs, and a read interest in txn, txn_payload, and
250 : hashes until it is destroyed. */
251 :
252 : FD_FN_UNUSED static fd_alut_interp_t *
253 : fd_alut_interp_new( fd_alut_interp_t * interp,
254 : fd_acct_addr_t * out_addrs,
255 : fd_txn_t const * txn,
256 : uchar const * txn_payload,
257 : fd_slot_hashes_t const * hashes,
258 213 : ulong slot ) {
259 213 : *interp = (fd_alut_interp_t){
260 213 : .out_accts_alt = out_addrs,
261 213 : .txn = txn,
262 213 : .txn_payload = txn_payload,
263 213 : .hashes = hashes,
264 213 : .slot = slot,
265 213 : .alut_idx = 0UL,
266 213 : .ro_indir_cnt = 0UL,
267 213 : .rw_indir_cnt = 0UL
268 213 : };
269 213 : return interp;
270 213 : }
271 :
272 : static inline int
273 114 : fd_alut_interp_done( fd_alut_interp_t const * interp ) {
274 114 : return interp->alut_idx >= interp->txn->addr_table_lookup_cnt;
275 114 : }
276 :
277 : /* fd_alut_interp_next resolves a subset of a txn's indirect account
278 : references. Resolves all addresses that are specified in the ALUT
279 : at index alut_idx. Returns one of:
280 : - FD_RUNTIME_EXECUTE_SUCCESS
281 : - FD_RUNTIME_TXN_ERR_INVALID_ADDRESS_LOOKUP_TABLE_OWNER
282 : - FD_RUNTIME_TXN_ERR_INVALID_ADDRESS_LOOKUP_TABLE_DATA
283 : - FD_RUNTIME_TXN_ERR_INVALID_ADDRESS_LOOKUP_TABLE_INDEX
284 : - FD_RUNTIME_TXN_ERR_ADDRESS_LOOKUP_TABLE_NOT_FOUND */
285 :
286 : FD_FN_UNUSED static int
287 : fd_alut_interp_next( fd_alut_interp_t * interp,
288 : void const * alut_addr,
289 : void const * alut_owner,
290 : uchar const * alut_data,
291 114 : ulong alut_data_sz ) {
292 114 : if( FD_UNLIKELY( fd_alut_interp_done( interp ) ) ) FD_LOG_CRIT(( "invariant violation" ));
293 114 : fd_acct_addr_t alut_addr_expected =
294 114 : FD_LOAD( fd_acct_addr_t, interp->txn_payload+fd_txn_get_address_tables_const( interp->txn )[ interp->alut_idx ].addr_off );
295 114 : if( FD_UNLIKELY( !fd_memeq( alut_addr, &alut_addr_expected, sizeof(fd_acct_addr_t) ) ) ) {
296 0 : FD_BASE58_ENCODE_32_BYTES( alut_addr, alut_addr_b58 );
297 0 : FD_BASE58_ENCODE_32_BYTES( alut_addr_expected.b, alut_addr_expected_b58 );
298 0 : FD_LOG_CRIT(( "expected address lookup table account %s but got %s",
299 0 : alut_addr_expected_b58, alut_addr_b58 ));
300 0 : }
301 114 : fd_txn_acct_addr_lut_t const * addr_lut =
302 114 : &fd_txn_get_address_tables_const( interp->txn )[ interp->alut_idx ];
303 :
304 : /* https://github.com/anza-xyz/agave/blob/368ea563c423b0a85cc317891187e15c9a321521/accounts-db/src/accounts.rs#L96-L114 */
305 114 : if( FD_UNLIKELY( !fd_memeq( alut_owner, fd_solana_address_lookup_table_program_id.key, sizeof(fd_pubkey_t) ) ) ) {
306 3 : return FD_RUNTIME_TXN_ERR_INVALID_ADDRESS_LOOKUP_TABLE_OWNER;
307 3 : }
308 :
309 : /* https://github.com/anza-xyz/agave/blob/368ea563c423b0a85cc317891187e15c9a321521/sdk/program/src/address_lookup_table/state.rs#L205-L209 */
310 111 : if( FD_UNLIKELY( alut_data_sz < FD_LOOKUP_TABLE_META_SIZE ) ) {
311 3 : return FD_RUNTIME_TXN_ERR_INVALID_ADDRESS_LOOKUP_TABLE_DATA;
312 3 : }
313 :
314 : /* https://github.com/anza-xyz/agave/blob/574bae8fefc0ed256b55340b9d87b7689bcdf222/accounts-db/src/accounts.rs#L141-L142 */
315 : /* https://github.com/anza-xyz/agave/blob/368ea563c423b0a85cc317891187e15c9a321521/sdk/program/src/address_lookup_table/state.rs#L197-L214 */
316 108 : fd_alut_meta_t meta;
317 108 : if( FD_UNLIKELY( fd_alut_state_decode( alut_data, FD_LOOKUP_TABLE_META_SIZE, &meta ) ) ) {
318 3 : return FD_RUNTIME_TXN_ERR_INVALID_ADDRESS_LOOKUP_TABLE_DATA;
319 3 : }
320 :
321 : /* https://github.com/anza-xyz/agave/blob/368ea563c423b0a85cc317891187e15c9a321521/sdk/program/src/address_lookup_table/state.rs#L200-L203 */
322 105 : if( FD_UNLIKELY( meta.discriminant!=FD_ALUT_STATE_DISC_LOOKUP_TABLE ) ) {
323 3 : return FD_RUNTIME_TXN_ERR_INVALID_ADDRESS_LOOKUP_TABLE_DATA;
324 3 : }
325 :
326 : /* Again probably an impossible case, but the ALUT data needs to be 32-byte aligned
327 : https://github.com/anza-xyz/agave/blob/368ea563c423b0a85cc317891187e15c9a321521/sdk/program/src/address_lookup_table/state.rs#L210-L214 */
328 102 : if( FD_UNLIKELY( (alut_data_sz - FD_LOOKUP_TABLE_META_SIZE) & 0x1fUL ) ) {
329 3 : return FD_RUNTIME_TXN_ERR_INVALID_ADDRESS_LOOKUP_TABLE_DATA;
330 3 : }
331 :
332 : /* https://github.com/anza-xyz/agave/blob/368ea563c423b0a85cc317891187e15c9a321521/accounts-db/src/accounts.rs#L101-L112 */
333 99 : fd_acct_addr_t const * lookup_addrs = fd_type_pun_const( alut_data+FD_LOOKUP_TABLE_META_SIZE );
334 99 : ulong lookup_addrs_cnt = (alut_data_sz - FD_LOOKUP_TABLE_META_SIZE) >> 5UL; // = (dlen - 56) / 32
335 :
336 : /* https://github.com/anza-xyz/agave/blob/368ea563c423b0a85cc317891187e15c9a321521/sdk/program/src/address_lookup_table/state.rs#L175-L176 */
337 99 : ulong active_addresses_len;
338 99 : int err = fd_alut_active_addresses_len(
339 99 : &meta,
340 99 : interp->slot,
341 99 : interp->hashes,
342 99 : lookup_addrs_cnt,
343 99 : &active_addresses_len
344 99 : );
345 99 : if( FD_UNLIKELY( err ) ) return err;
346 :
347 : /* https://github.com/anza-xyz/solana-sdk/blob/address-lookup-table-interface%40v3.0.1/address-lookup-table-interface/src/state.rs#L208-L211 */
348 96 : if( FD_UNLIKELY( active_addresses_len>lookup_addrs_cnt ) ) {
349 0 : return FD_RUNTIME_TXN_ERR_INVALID_ADDRESS_LOOKUP_TABLE_DATA;
350 0 : }
351 :
352 : /* https://github.com/anza-xyz/agave/blob/368ea563c423b0a85cc317891187e15c9a321521/sdk/program/src/address_lookup_table/state.rs#L169-L182 */
353 96 : uchar const * writable_lut_idxs = interp->txn_payload + addr_lut->writable_off;
354 216 : for( ulong j=0UL; j<addr_lut->writable_cnt; j++ ) {
355 : /* https://github.com/anza-xyz/agave/blob/368ea563c423b0a85cc317891187e15c9a321521/sdk/program/src/address_lookup_table/state.rs#L177-L181 */
356 126 : if( writable_lut_idxs[j] >= active_addresses_len ) {
357 6 : return FD_RUNTIME_TXN_ERR_INVALID_ADDRESS_LOOKUP_TABLE_INDEX;
358 6 : }
359 120 : interp->out_accts_alt[ interp->rw_indir_cnt++ ] = lookup_addrs[ writable_lut_idxs[ j ] ];
360 120 : }
361 :
362 90 : uchar const * readonly_lut_idxs = interp->txn_payload + addr_lut->readonly_off;
363 90 : fd_acct_addr_t * out_accts_ro = interp->out_accts_alt + interp->txn->addr_table_adtl_writable_cnt;
364 207 : for( ulong j=0UL; j<addr_lut->readonly_cnt; j++ ) {
365 : /* https://github.com/anza-xyz/agave/blob/368ea563c423b0a85cc317891187e15c9a321521/sdk/program/src/address_lookup_table/state.rs#L177-L181 */
366 123 : if( readonly_lut_idxs[j] >= active_addresses_len ) {
367 6 : return FD_RUNTIME_TXN_ERR_INVALID_ADDRESS_LOOKUP_TABLE_INDEX;
368 6 : }
369 117 : out_accts_ro[ interp->ro_indir_cnt++ ] = lookup_addrs[ readonly_lut_idxs[ j ] ];
370 117 : }
371 :
372 84 : interp->alut_idx++;
373 84 : return FD_RUNTIME_EXECUTE_SUCCESS;
374 90 : }
375 :
376 : FD_PROTOTYPES_END
377 :
378 :
379 : #endif /* HEADER_fd_src_flamenco_runtime_fd_alut_h */
|