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