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 192 : #define FD_ADDRLUT_STATUS_ACTIVATED (0)
16 120 : #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 498 : #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 150 : #define FD_ALUT_STATE_DISC_UNINITIALIZED (0U)
27 234 : #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 117 : ulong bufsz ) {
74 117 : uchar * const _payload = buf;
75 117 : ulong const _payload_sz = bufsz;
76 117 : ulong _i = 0UL;
77 :
78 705 : # define CHECK( cond ) { if( FD_UNLIKELY( !(cond) ) ) { return -1; } }
79 705 : # define CHECK_LEFT( n ) CHECK( (n)<=(_payload_sz-_i) )
80 705 : # define INC( n ) (_i += (ulong)(n))
81 120 : # define CURSOR (_payload+_i)
82 :
83 117 : CHECK_LEFT( 4UL ); FD_STORE( uint, CURSOR, meta->discriminant ); INC( 4UL );
84 :
85 117 : if( meta->discriminant==FD_ALUT_STATE_DISC_LOOKUP_TABLE ) {
86 117 : CHECK_LEFT( 8UL ); FD_STORE( ulong, CURSOR, meta->deactivation_slot ); INC( 8UL );
87 117 : CHECK_LEFT( 8UL ); FD_STORE( ulong, CURSOR, meta->last_extended_slot ); INC( 8UL );
88 117 : CHECK_LEFT( 1UL ); FD_STORE( uchar, CURSOR, meta->last_extended_slot_start_index ); INC( 1UL );
89 :
90 117 : CHECK_LEFT( 1UL ); FD_STORE( uchar, CURSOR, (uchar)!!meta->has_authority ); INC( 1UL );
91 :
92 117 : 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 117 : CHECK_LEFT( 2UL ); FD_STORE( ushort, CURSOR, (ushort)0 ); INC( 2UL );
98 117 : }
99 :
100 117 : fd_memset( CURSOR, 0, _payload_sz-_i ); /* zero-pad the rest of the buffer */
101 :
102 117 : # undef CHECK
103 117 : # undef CHECK_LEFT
104 117 : # undef INC
105 117 : # undef CURSOR
106 :
107 117 : return 0;
108 117 : }
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 150 : fd_alut_meta_t * out ) {
119 150 : uchar const * _payload = data;
120 150 : ulong const _payload_sz = data_sz;
121 150 : ulong _i = 0UL;
122 :
123 1146 : # define CHECK( cond ) { if( FD_UNLIKELY( !(cond) ) ) { return -1; } }
124 855 : # define CHECK_LEFT( n ) CHECK( (n)<=(_payload_sz-_i) )
125 849 : # define INC( n ) (_i += (ulong)(n))
126 150 : # define CURSOR (_payload+_i)
127 :
128 150 : CHECK_LEFT( 4UL ); uint disc = FD_LOAD( uint, CURSOR ); INC( 4UL );
129 150 : out->discriminant = disc;
130 :
131 150 : if( disc==FD_ALUT_STATE_DISC_UNINITIALIZED ) return 0;
132 :
133 147 : CHECK( disc==FD_ALUT_STATE_DISC_LOOKUP_TABLE );
134 :
135 144 : CHECK_LEFT( 8UL ); out->deactivation_slot = FD_LOAD( ulong, CURSOR ); INC( 8UL );
136 144 : CHECK_LEFT( 8UL ); out->last_extended_slot = FD_LOAD( ulong, CURSOR ); INC( 8UL );
137 144 : 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 144 : CHECK_LEFT( 1UL ); uchar has_auth = FD_LOAD( uchar, CURSOR ); INC( 1UL );
141 144 : CHECK( !(has_auth & (uchar)~1U) );
142 126 : out->has_authority = has_auth;
143 :
144 126 : 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 126 : CHECK_LEFT( 2UL ); INC( 2UL );
152 :
153 120 : # undef CHECK
154 120 : # undef CHECK_LEFT
155 120 : # undef INC
156 120 : # undef CURSOR
157 :
158 120 : return 0;
159 126 : }
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 111 : fd_slot_hashes_t const * slot_hashes ) {
192 111 : if( state->deactivation_slot==ULONG_MAX ) {
193 96 : return FD_ADDRLUT_STATUS_ACTIVATED;
194 96 : }
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 111 : fd_slot_hashes_t const * slot_hashes ) {
213 111 : uchar status = fd_alut_status( self, current_slot, slot_hashes );
214 111 : switch( status ) {
215 96 : case FD_ADDRLUT_STATUS_ACTIVATED:
216 108 : case FD_ADDRLUT_STATUS_DEACTIVATING:
217 108 : 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 111 : }
223 111 : }
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 111 : ulong * active_addresses_len /* out */ ) {
234 111 : 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 108 : *active_addresses_len = ( current_slot > self->last_extended_slot )
239 108 : ? addresses_len
240 108 : : self->last_extended_slot_start_index;
241 :
242 108 : return FD_RUNTIME_EXECUTE_SUCCESS;
243 111 : }
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 207 : ulong slot ) {
259 207 : *interp = (fd_alut_interp_t){
260 207 : .out_accts_alt = out_addrs,
261 207 : .txn = txn,
262 207 : .txn_payload = txn_payload,
263 207 : .hashes = hashes,
264 207 : .slot = slot,
265 207 : .alut_idx = 0UL,
266 207 : .ro_indir_cnt = 0UL,
267 207 : .rw_indir_cnt = 0UL
268 207 : };
269 207 : return interp;
270 207 : }
271 :
272 : static inline int
273 126 : fd_alut_interp_done( fd_alut_interp_t const * interp ) {
274 126 : return interp->alut_idx >= interp->txn->addr_table_lookup_cnt;
275 126 : }
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 126 : ulong alut_data_sz ) {
292 126 : if( FD_UNLIKELY( fd_alut_interp_done( interp ) ) ) FD_LOG_CRIT(( "invariant violation" ));
293 126 : fd_acct_addr_t alut_addr_expected =
294 126 : FD_LOAD( fd_acct_addr_t, interp->txn_payload+fd_txn_get_address_tables_const( interp->txn )[ interp->alut_idx ].addr_off );
295 126 : 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 126 : fd_txn_acct_addr_lut_t const * addr_lut =
302 126 : &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 126 : 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 123 : 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 120 : fd_alut_meta_t meta;
317 120 : 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 117 : 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 114 : 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 111 : fd_acct_addr_t const * lookup_addrs = fd_type_pun_const( alut_data+FD_LOOKUP_TABLE_META_SIZE );
334 111 : 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 111 : ulong active_addresses_len;
338 111 : int err = fd_alut_active_addresses_len(
339 111 : &meta,
340 111 : interp->slot,
341 111 : interp->hashes,
342 111 : lookup_addrs_cnt,
343 111 : &active_addresses_len
344 111 : );
345 111 : 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 108 : 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 108 : uchar const * writable_lut_idxs = interp->txn_payload + addr_lut->writable_off;
354 252 : 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 156 : if( writable_lut_idxs[j] >= active_addresses_len ) {
357 12 : return FD_RUNTIME_TXN_ERR_INVALID_ADDRESS_LOOKUP_TABLE_INDEX;
358 12 : }
359 144 : interp->out_accts_alt[ interp->rw_indir_cnt++ ] = lookup_addrs[ writable_lut_idxs[ j ] ];
360 144 : }
361 :
362 96 : uchar const * readonly_lut_idxs = interp->txn_payload + addr_lut->readonly_off;
363 96 : fd_acct_addr_t * out_accts_ro = interp->out_accts_alt + interp->txn->addr_table_adtl_writable_cnt;
364 213 : 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 90 : interp->alut_idx++;
373 90 : return FD_RUNTIME_EXECUTE_SUCCESS;
374 96 : }
375 :
376 : FD_PROTOTYPES_END
377 :
378 :
379 : #endif /* HEADER_fd_src_flamenco_runtime_fd_alut_h */
|