Line data Source code
1 : #include "fd_solfuzz.h"
2 : #include "fd_solfuzz_private.h"
3 : #include "fd_txn_harness.h"
4 : #include "fd_dump_pb.h"
5 : #include "../fd_runtime.h"
6 : #include "../../accdb/fd_accdb_admin_v1.h"
7 : #include "../../accdb/fd_accdb_impl_v1.h"
8 : #include "../../log_collector/fd_log_collector.h"
9 : #include "../fd_system_ids.h"
10 :
11 : /* Macros to append data to construct a serialized transaction
12 : without exceeding bounds */
13 0 : #define FD_CHECKED_ADD_TO_TXN_DATA( _begin, _cur_data, _to_add, _sz ) __extension__({ \
14 0 : if( FD_UNLIKELY( (*_cur_data)+_sz>_begin+FD_TXN_MTU ) ) return ULONG_MAX; \
15 0 : fd_memcpy( *_cur_data, _to_add, _sz ); \
16 0 : *_cur_data += _sz; \
17 0 : })
18 :
19 0 : #define FD_CHECKED_ADD_CU16_TO_TXN_DATA( _begin, _cur_data, _to_add ) __extension__({ \
20 0 : do { \
21 0 : uchar _buf[3]; \
22 0 : fd_bincode_encode_ctx_t _encode_ctx = { .data = _buf, .dataend = _buf+3 }; \
23 0 : fd_bincode_compact_u16_encode( &_to_add, &_encode_ctx ); \
24 0 : ulong _sz = (ulong) ((uchar *)_encode_ctx.data - _buf ); \
25 0 : FD_CHECKED_ADD_TO_TXN_DATA( _begin, _cur_data, _buf, _sz ); \
26 0 : } while(0); \
27 0 : })
28 :
29 : /* Retrieves the slot number from the clock sysvar account within the
30 : txn context. Throws FD_LOG_ERR if the clock sysvar is not found
31 : or is malformed. */
32 : static ulong
33 0 : fd_solfuzz_pb_txn_ctx_get_slot( fd_exec_test_txn_context_t const * test_ctx ) {
34 0 : for( ulong i=0UL; i<test_ctx->account_shared_data_count; i++ ) {
35 0 : if( !memcmp( &test_ctx->account_shared_data[i].address, &fd_sysvar_clock_id, sizeof(fd_pubkey_t) ) ) {
36 0 : FD_TEST( test_ctx->account_shared_data[i].data->size==sizeof(fd_sol_sysvar_clock_t) );
37 0 : return FD_LOAD( ulong, test_ctx->account_shared_data[i].data->bytes );
38 0 : }
39 0 : }
40 0 : FD_LOG_ERR(( "invariant violation: clock sysvar account not found in txn context" ));
41 0 : }
42 :
43 : static void
44 0 : fd_solfuzz_txn_ctx_destroy( fd_solfuzz_runner_t * runner ) {
45 0 : fd_accdb_v1_clear( runner->accdb_admin );
46 0 : fd_progcache_clear( runner->progcache_admin );
47 :
48 : /* In order to check for leaks in the workspace, we need to compact the
49 : allocators. Without doing this, empty superblocks may be retained
50 : by the fd_alloc instance, which mean we cannot check for leaks. */
51 0 : fd_alloc_compact( fd_accdb_user_v1_funk( runner->accdb )->alloc );
52 0 : fd_alloc_compact( runner->progcache_admin->funk->alloc );
53 0 : }
54 :
55 : /* Creates transaction execution context for a single test case.
56 : Returns a parsed txn descriptor on success and NULL on failure. */
57 : static fd_txn_p_t *
58 : fd_solfuzz_pb_txn_ctx_create( fd_solfuzz_runner_t * runner,
59 0 : fd_exec_test_txn_context_t const * test_ctx ) {
60 0 : fd_accdb_user_t * accdb = runner->accdb;
61 :
62 : /* Set up the funk transaction */
63 0 : ulong slot = fd_solfuzz_pb_txn_ctx_get_slot( test_ctx );
64 0 : fd_funk_txn_xid_t xid = { .ul = { slot, runner->bank->data->idx } };
65 0 : fd_funk_txn_xid_t parent_xid; fd_funk_txn_xid_set_root( &parent_xid );
66 0 : fd_accdb_attach_child ( runner->accdb_admin, &parent_xid, &xid );
67 0 : fd_progcache_txn_attach_child( runner->progcache_admin, &parent_xid, &xid );
68 :
69 : /* Initialize bank from input txn bank */
70 0 : fd_banks_clear_bank( runner->banks, runner->bank, 64UL );
71 0 : FD_TEST( test_ctx->has_bank );
72 0 : fd_exec_test_txn_bank_t const * txn_bank = &test_ctx->bank;
73 :
74 : /* Initialize blockhash queue */
75 0 : ulong blockhash_seed; FD_TEST( fd_rng_secure( &blockhash_seed, sizeof(ulong) ) );
76 0 : fd_blockhashes_t * blockhashes = fd_blockhashes_init( fd_bank_block_hash_queue_modify( runner->bank ), blockhash_seed );
77 0 : for( uint i=0UL; i<txn_bank->blockhash_queue_count; i++ ) {
78 0 : fd_exec_test_blockhash_queue_entry_t const * entry = &txn_bank->blockhash_queue[i];
79 :
80 0 : fd_hash_t hash = FD_LOAD( fd_hash_t, entry->blockhash );
81 0 : ulong lamports_per_signature = entry->lamports_per_signature;
82 :
83 0 : fd_blockhash_info_t * blockhash = fd_blockhashes_push_new( blockhashes, &hash );
84 0 : blockhash->fee_calculator = (fd_fee_calculator_t){
85 0 : .lamports_per_signature = lamports_per_signature
86 0 : };
87 0 : }
88 :
89 : /* RBH lamports per signature. In the Agave harness this is set inside
90 : the fee rate governor itself. */
91 0 : fd_bank_rbh_lamports_per_sig_set( runner->bank, txn_bank->rbh_lamports_per_signature );
92 :
93 : /* Fee rate governor */
94 0 : FD_TEST( txn_bank->has_fee_rate_governor );
95 0 : fd_fee_rate_governor_t * fee_rate_governor = fd_bank_fee_rate_governor_modify( runner->bank );
96 0 : *fee_rate_governor = (fd_fee_rate_governor_t){
97 0 : .target_lamports_per_signature = txn_bank->fee_rate_governor.target_lamports_per_signature,
98 0 : .target_signatures_per_slot = txn_bank->fee_rate_governor.target_signatures_per_slot,
99 0 : .min_lamports_per_signature = txn_bank->fee_rate_governor.min_lamports_per_signature,
100 0 : .max_lamports_per_signature = txn_bank->fee_rate_governor.max_lamports_per_signature,
101 0 : .burn_percent = (uchar)txn_bank->fee_rate_governor.burn_percent,
102 0 : };
103 :
104 : /* Slot and parent slot */
105 0 : fd_bank_slot_set( runner->bank, slot );
106 0 : fd_bank_parent_slot_set( runner->bank, slot-1UL );
107 :
108 : /* Total epoch stake */
109 0 : fd_bank_total_epoch_stake_set( runner->bank, txn_bank->total_epoch_stake );
110 :
111 : /* Epoch schedule */
112 0 : FD_TEST( txn_bank->has_epoch_schedule );
113 0 : fd_epoch_schedule_t * epoch_schedule = fd_bank_epoch_schedule_modify( runner->bank );
114 0 : *epoch_schedule = (fd_epoch_schedule_t){
115 0 : .slots_per_epoch = txn_bank->epoch_schedule.slots_per_epoch,
116 0 : .leader_schedule_slot_offset = txn_bank->epoch_schedule.leader_schedule_slot_offset,
117 0 : .warmup = txn_bank->epoch_schedule.warmup,
118 0 : .first_normal_epoch = txn_bank->epoch_schedule.first_normal_epoch,
119 0 : .first_normal_slot = txn_bank->epoch_schedule.first_normal_slot
120 0 : };
121 :
122 : /* Rent */
123 0 : FD_TEST( txn_bank->has_rent );
124 0 : fd_rent_t * rent = fd_bank_rent_modify( runner->bank );
125 0 : *rent = (fd_rent_t){
126 0 : .lamports_per_uint8_year = txn_bank->rent.lamports_per_byte_year,
127 0 : .exemption_threshold = txn_bank->rent.exemption_threshold,
128 0 : .burn_percent = (uchar)txn_bank->rent.burn_percent
129 0 : };
130 :
131 : /* Features */
132 0 : FD_TEST( txn_bank->has_features );
133 0 : fd_exec_test_feature_set_t const * feature_set = &txn_bank->features;
134 0 : fd_features_t * features_bm = fd_bank_features_modify( runner->bank );
135 0 : if( !fd_solfuzz_pb_restore_features( features_bm, feature_set ) ) {
136 0 : return NULL;
137 0 : }
138 :
139 : /* Epoch */
140 0 : fd_bank_epoch_set( runner->bank, txn_bank->epoch );
141 :
142 : /* Load account states into funk (note this is different from the account keys):
143 : Account state = accounts to populate Funk
144 : Account keys = account keys that the transaction needs */
145 0 : for( ulong i = 0; i < test_ctx->account_shared_data_count; i++ ) {
146 : /* Load the accounts into the account manager
147 : Borrowed accounts get reset anyways - we just need to load the account somewhere */
148 0 : fd_solfuzz_pb_load_account( runner->runtime, accdb, &xid, &test_ctx->account_shared_data[i], i );
149 0 : }
150 :
151 :
152 0 : fd_bank_ticks_per_slot_set( runner->bank, 64 );
153 0 : fd_bank_slots_per_year_set( runner->bank, SECONDS_PER_YEAR * (1000000000.0 / (double)6250000) / (double)(fd_bank_ticks_per_slot_get( runner->bank )) );
154 :
155 : /* Restore sysvars from account context */
156 0 : fd_sysvar_cache_restore_fuzz( runner->bank, runner->accdb, &xid );
157 :
158 : /* Create the raw txn (https://solana.com/docs/core/transactions#transaction-size) */
159 0 : fd_txn_p_t * txn = fd_spad_alloc( runner->spad, alignof(fd_txn_p_t), sizeof(fd_txn_p_t) );
160 0 : ulong msg_sz = fd_solfuzz_pb_txn_serialize( txn->payload, &test_ctx->tx );
161 0 : if( FD_UNLIKELY( msg_sz==ULONG_MAX ) ) {
162 0 : return NULL;
163 0 : }
164 :
165 : /* Set up txn descriptor from raw data */
166 0 : if( FD_UNLIKELY( !fd_txn_parse( txn->payload, msg_sz, TXN( txn ), NULL ) ) ) {
167 0 : return NULL;
168 0 : }
169 :
170 0 : txn->payload_sz = msg_sz;
171 :
172 0 : return txn;
173 0 : }
174 :
175 : ulong
176 : fd_solfuzz_pb_txn_serialize( uchar * txn_raw_begin,
177 0 : fd_exec_test_sanitized_transaction_t const * tx ) {
178 0 : uchar * txn_raw_cur_ptr = txn_raw_begin;
179 :
180 : /* Compact array of signatures (https://solana.com/docs/core/transactions#transaction)
181 : Note that although documentation interchangably refers to the signature cnt as a compact-u16
182 : and a u8, the max signature cnt is capped at 48 (due to txn size limits), so u8 and compact-u16
183 : is represented the same way anyways and can be parsed identically. */
184 : // Note: always create a valid txn with 1+ signatures, add an empty signature if none is provided
185 0 : uchar signature_cnt = fd_uchar_max( 1, (uchar) tx->signatures_count );
186 0 : FD_CHECKED_ADD_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, &signature_cnt, sizeof(uchar) );
187 0 : for( uchar i = 0; i < signature_cnt; ++i ) {
188 0 : fd_signature_t sig = {0};
189 0 : if( tx->signatures && tx->signatures[i] ) sig = FD_LOAD( fd_signature_t, tx->signatures[i]->bytes );
190 0 : FD_CHECKED_ADD_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, &sig, FD_TXN_SIGNATURE_SZ );
191 0 : }
192 :
193 : /* Message */
194 : /* For v0 transactions, the highest bit of the num_required_signatures is set, and an extra byte is used for the version.
195 : https://solanacookbook.com/guides/versioned-transactions.html#versioned-transactions-transactionv0
196 :
197 : We will always create a transaction with at least 1 signature, and cap the signature count to 127 to avoid
198 : collisions with the header_b0 tag. */
199 0 : uchar num_required_signatures = fd_uchar_max( 1, fd_uchar_min( 127, (uchar) tx->message.header.num_required_signatures ) );
200 0 : if( !tx->message.is_legacy ) {
201 0 : uchar header_b0 = (uchar) 0x80UL;
202 0 : FD_CHECKED_ADD_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, &header_b0, sizeof(uchar) );
203 0 : }
204 :
205 : /* Header (3 bytes) (https://solana.com/docs/core/transactions#message-header) */
206 0 : FD_CHECKED_ADD_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, &num_required_signatures, sizeof(uchar) );
207 0 : FD_CHECKED_ADD_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, &tx->message.header.num_readonly_signed_accounts, sizeof(uchar) );
208 0 : FD_CHECKED_ADD_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, &tx->message.header.num_readonly_unsigned_accounts, sizeof(uchar) );
209 :
210 : /* Compact array of account addresses (https://solana.com/docs/core/transactions#compact-array-format) */
211 : // Array length is a compact u16
212 0 : ushort num_acct_keys = (ushort) tx->message.account_keys_count;
213 0 : FD_CHECKED_ADD_CU16_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, num_acct_keys );
214 0 : for( ushort i = 0; i < num_acct_keys; ++i ) {
215 0 : FD_CHECKED_ADD_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, tx->message.account_keys[i]->bytes, sizeof(fd_pubkey_t) );
216 0 : }
217 :
218 : /* Recent blockhash (32 bytes) (https://solana.com/docs/core/transactions#recent-blockhash) */
219 : // Note: add an empty blockhash if none is provided
220 0 : fd_hash_t msg_rbh = {0};
221 0 : if( tx->message.recent_blockhash ) msg_rbh = FD_LOAD( fd_hash_t, tx->message.recent_blockhash->bytes );
222 0 : FD_CHECKED_ADD_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, &msg_rbh, sizeof(fd_hash_t) );
223 :
224 : /* Compact array of instructions (https://solana.com/docs/core/transactions#array-of-instructions) */
225 : // Instruction count is a compact u16
226 0 : ushort instr_count = (ushort) tx->message.instructions_count;
227 0 : FD_CHECKED_ADD_CU16_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, instr_count );
228 0 : for( ushort i = 0; i < instr_count; ++i ) {
229 : // Program ID index
230 0 : uchar program_id_index = (uchar) tx->message.instructions[i].program_id_index;
231 0 : FD_CHECKED_ADD_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, &program_id_index, sizeof(uchar) );
232 :
233 : // Compact array of account addresses
234 0 : ushort acct_count = (ushort) tx->message.instructions[i].accounts_count;
235 0 : FD_CHECKED_ADD_CU16_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, acct_count );
236 0 : for( ushort j = 0; j < acct_count; ++j ) {
237 0 : uchar account_index = (uchar) tx->message.instructions[i].accounts[j];
238 0 : FD_CHECKED_ADD_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, &account_index, sizeof(uchar) );
239 0 : }
240 :
241 : // Compact array of 8-bit data
242 0 : pb_bytes_array_t * data = tx->message.instructions[i].data;
243 0 : ushort data_len;
244 0 : if( data ) {
245 0 : data_len = (ushort) data->size;
246 0 : FD_CHECKED_ADD_CU16_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, data_len );
247 0 : FD_CHECKED_ADD_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, data->bytes, data_len );
248 0 : } else {
249 0 : data_len = 0;
250 0 : FD_CHECKED_ADD_CU16_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, data_len );
251 0 : }
252 0 : }
253 :
254 : /* Address table lookups (N/A for legacy transactions) */
255 0 : ushort addr_table_cnt = 0;
256 0 : if( !tx->message.is_legacy ) {
257 : /* Compact array of address table lookups (https://solanacookbook.com/guides/versioned-transactions.html#compact-array-of-address-table-lookups) */
258 : // NOTE: The diagram is slightly wrong - the account key is a 32 byte pubkey, not a u8
259 0 : addr_table_cnt = (ushort) tx->message.address_table_lookups_count;
260 0 : FD_CHECKED_ADD_CU16_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, addr_table_cnt );
261 0 : for( ushort i = 0; i < addr_table_cnt; ++i ) {
262 : // Account key
263 0 : FD_CHECKED_ADD_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, tx->message.address_table_lookups[i].account_key, sizeof(fd_pubkey_t) );
264 :
265 : // Compact array of writable indexes
266 0 : ushort writable_count = (ushort) tx->message.address_table_lookups[i].writable_indexes_count;
267 0 : FD_CHECKED_ADD_CU16_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, writable_count );
268 0 : for( ushort j = 0; j < writable_count; ++j ) {
269 0 : uchar writable_index = (uchar) tx->message.address_table_lookups[i].writable_indexes[j];
270 0 : FD_CHECKED_ADD_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, &writable_index, sizeof(uchar) );
271 0 : }
272 :
273 : // Compact array of readonly indexes
274 0 : ushort readonly_count = (ushort) tx->message.address_table_lookups[i].readonly_indexes_count;
275 0 : FD_CHECKED_ADD_CU16_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, readonly_count );
276 0 : for( ushort j = 0; j < readonly_count; ++j ) {
277 0 : uchar readonly_index = (uchar) tx->message.address_table_lookups[i].readonly_indexes[j];
278 0 : FD_CHECKED_ADD_TO_TXN_DATA( txn_raw_begin, &txn_raw_cur_ptr, &readonly_index, sizeof(uchar) );
279 0 : }
280 0 : }
281 0 : }
282 :
283 0 : return (ulong)(txn_raw_cur_ptr - txn_raw_begin);
284 0 : }
285 :
286 : void
287 : fd_solfuzz_txn_ctx_exec( fd_solfuzz_runner_t * runner,
288 : fd_runtime_t * runtime,
289 : fd_txn_in_t const * txn_in,
290 : int * exec_res,
291 0 : fd_txn_out_t * txn_out ) {
292 :
293 0 : txn_out->err.is_committable = 1;
294 :
295 0 : runtime->log.enable_vm_tracing = runner->enable_vm_tracing;
296 0 : uchar * tracing_mem = NULL;
297 0 : if( runner->enable_vm_tracing ) {
298 0 : tracing_mem = fd_spad_alloc_check( runner->spad, FD_RUNTIME_VM_TRACE_STATIC_ALIGN, FD_RUNTIME_VM_TRACE_STATIC_FOOTPRINT * FD_MAX_INSTRUCTION_STACK_DEPTH );
299 0 : }
300 :
301 0 : runtime->accdb = runner->accdb;
302 0 : runtime->progcache = runner->progcache;
303 0 : runtime->status_cache = NULL;
304 0 : runtime->log.tracing_mem = tracing_mem;
305 0 : runtime->log.dumping_mem = NULL;
306 0 : runtime->log.capture_ctx = NULL;
307 0 : runtime->log.dump_proto_ctx = NULL;
308 0 : runtime->log.txn_dump_ctx = NULL;
309 :
310 0 : fd_runtime_prepare_and_execute_txn( runtime, runner->bank, txn_in, txn_out );
311 0 : *exec_res = txn_out->err.txn_err;
312 0 : }
313 :
314 : ulong
315 : fd_solfuzz_pb_txn_run( fd_solfuzz_runner_t * runner,
316 : void const * input_,
317 : void ** output_,
318 : void * output_buf,
319 0 : ulong output_bufsz ) {
320 0 : fd_exec_test_txn_context_t const * input = fd_type_pun_const( input_ );
321 0 : fd_exec_test_txn_result_t ** output = fd_type_pun( output_ );
322 :
323 0 : FD_SPAD_FRAME_BEGIN( runner->spad ) {
324 :
325 : /* Setup the transaction context */
326 0 : fd_txn_p_t * txn = fd_solfuzz_pb_txn_ctx_create( runner, input );
327 0 : if( FD_UNLIKELY( txn==NULL ) ) {
328 0 : fd_solfuzz_txn_ctx_destroy( runner );
329 0 : return 0UL;
330 0 : }
331 :
332 : /* Execute the transaction against the runtime */
333 0 : int exec_res = 0;
334 0 : fd_runtime_t * runtime = runner->runtime;
335 0 : fd_txn_in_t * txn_in = fd_spad_alloc( runner->spad, alignof(fd_txn_in_t), sizeof(fd_txn_in_t) );
336 0 : fd_txn_out_t * txn_out = fd_spad_alloc( runner->spad, alignof(fd_txn_out_t), sizeof(fd_txn_out_t) );
337 0 : fd_log_collector_t * log = fd_spad_alloc( runner->spad, alignof(fd_log_collector_t), sizeof(fd_log_collector_t) );
338 0 : runtime->log.log_collector = log;
339 0 : runtime->acc_pool = runner->acc_pool;
340 0 : txn_in->txn = txn;
341 0 : txn_in->bundle.is_bundle = 0;
342 0 : fd_solfuzz_txn_ctx_exec( runner, runtime, txn_in, &exec_res, txn_out );
343 :
344 : /* Build result directly into the caller-owned output_buf */
345 0 : fd_exec_test_txn_result_t * txn_result = NULL;
346 0 : ulong result_sz = create_txn_result_protobuf_from_txn(
347 0 : &txn_result,
348 0 : output_buf,
349 0 : output_bufsz,
350 0 : txn_in,
351 0 : txn_out,
352 0 : runner->bank,
353 0 : exec_res
354 0 : );
355 :
356 0 : txn_out->err.is_committable = 0;
357 0 : fd_runtime_cancel_txn( runner->runtime, txn_out );
358 0 : fd_solfuzz_txn_ctx_destroy( runner );
359 :
360 0 : *output = txn_result;
361 0 : return result_sz;
362 0 : } FD_SPAD_FRAME_END;
363 0 : }
|