@@ -1069,17 +1069,22 @@ mod tests {
1069
1069
rollback_accounts:: RollbackAccounts ,
1070
1070
} ,
1071
1071
agave_reserved_account_keys:: ReservedAccountKeys ,
1072
+ assert_matches:: assert_matches,
1073
+ itertools:: Itertools ,
1074
+ lazy_static:: lazy_static,
1072
1075
solana_account:: { create_account_shared_data_for_test, WritableAccount } ,
1073
1076
solana_clock:: Clock ,
1074
1077
solana_compute_budget_interface:: ComputeBudgetInstruction ,
1075
1078
solana_epoch_schedule:: EpochSchedule ,
1076
1079
solana_fee_calculator:: FeeCalculator ,
1077
1080
solana_fee_structure:: FeeDetails ,
1078
1081
solana_hash:: Hash ,
1082
+ solana_instruction:: { AccountMeta , Instruction } ,
1079
1083
solana_keypair:: Keypair ,
1080
1084
solana_message:: { LegacyMessage , Message , MessageHeader , SanitizedMessage } ,
1081
1085
solana_nonce as nonce,
1082
1086
solana_program_runtime:: {
1087
+ declare_process_instruction,
1083
1088
execution_budget:: {
1084
1089
SVMTransactionExecutionAndFeeBudgetLimits , SVMTransactionExecutionBudget ,
1085
1090
} ,
@@ -1088,12 +1093,14 @@ mod tests {
1088
1093
solana_rent:: Rent ,
1089
1094
solana_rent_collector:: { RentCollector , RENT_EXEMPT_RENT_EPOCH } ,
1090
1095
solana_rent_debits:: RentDebits ,
1096
+ solana_sdk:: instruction:: InstructionError ,
1091
1097
solana_sdk_ids:: { bpf_loader, system_program, sysvar} ,
1092
1098
solana_signature:: Signature ,
1093
1099
solana_svm_callback:: { AccountState , InvokeContextCallback } ,
1094
1100
solana_transaction:: { sanitized:: SanitizedTransaction , Transaction } ,
1095
1101
solana_transaction_context:: TransactionContext ,
1096
1102
solana_transaction_error:: { TransactionError , TransactionError :: DuplicateInstruction } ,
1103
+ std:: convert:: TryInto ,
1097
1104
test_case:: test_case,
1098
1105
} ;
1099
1106
@@ -1292,6 +1299,165 @@ mod tests {
1292
1299
) ;
1293
1300
}
1294
1301
1302
+ #[ test_case( 0 ; "Throw in program 0" ) ]
1303
+ #[ test_case( 1 ; "Throw in program 1" ) ]
1304
+ #[ test_case( 2 ; "Throw in program 2" ) ]
1305
+ #[ test_case( 3 ; "Throw in program 3" ) ]
1306
+ #[ test_case( 4 ; "Throw in program 4" ) ]
1307
+ #[ test_case( 5 ; "Throw in program 5" ) ]
1308
+ #[ test_case( 6 ; "Throw in program 6" ) ]
1309
+ fn test_instruction_error_carries_responsible_program_account_index (
1310
+ index_of_program_that_should_throw_exception : u8 ,
1311
+ ) {
1312
+ lazy_static ! {
1313
+ static ref PROGRAM_ADDRESSES : [ Pubkey ; 7 ] =
1314
+ std:: array:: from_fn( |_| Pubkey :: new_unique( ) ) ;
1315
+ }
1316
+ // This mock program takes in a list of programs and two bytes of data:
1317
+ // (1) the index of the program that should throw an error
1318
+ // (2) the index of the program being called
1319
+ //
1320
+ // The programs get executed - two at each CPI call depth - like this:
1321
+ //
1322
+ // Program 0
1323
+ // -- Program 1
1324
+ // -- Program 2
1325
+ // ---- Program 3
1326
+ // ---- Program 4
1327
+ // ------ Program 5
1328
+ // ------ Program 6
1329
+ declare_process_instruction ! ( MockBuiltin , 1 /* cu_to_consume */ , |invoke_context| {
1330
+ let transaction_context = invoke_context. transaction_context. clone( ) ;
1331
+ let instruction_context = transaction_context. get_current_instruction_context( ) ?;
1332
+
1333
+ let stack_height: u8 = invoke_context. get_stack_height( ) . try_into( ) . unwrap( ) ;
1334
+ let index_of_program_that_must_throw_exception =
1335
+ instruction_context. get_instruction_data( ) [ 0 ] as usize ;
1336
+ let current_program_index = instruction_context. get_instruction_data( ) [ 1 ] as usize ;
1337
+
1338
+ // Ensure that the program the instruction data claims to be running is the program that
1339
+ // is actually running.
1340
+ let actual_program_address = * instruction_context
1341
+ . get_last_program_key( & transaction_context)
1342
+ . unwrap( ) ;
1343
+ assert_eq!(
1344
+ actual_program_address,
1345
+ PROGRAM_ADDRESSES [ current_program_index] ,
1346
+ "The address of the program at index {} that the instruction data claims to be running ({}) is not the program that is actually running ({})" ,
1347
+ current_program_index,
1348
+ PROGRAM_ADDRESSES [ current_program_index] ,
1349
+ actual_program_address,
1350
+ ) ;
1351
+
1352
+ if current_program_index == index_of_program_that_must_throw_exception {
1353
+ return Err ( InstructionError :: Custom ( 0xdeadbeef ) ) ;
1354
+ }
1355
+
1356
+ if stack_height < 4 && current_program_index % 2 == 0 {
1357
+ // Every odd program does CPIs unless it has reached the maximum CPI stack depth.
1358
+ let mut last_result = Ok ( ( ) ) ;
1359
+ for ii in 1 ..3 {
1360
+ let next_program_index = current_program_index. checked_add( ii) . unwrap( ) ;
1361
+ let next_program_address = PROGRAM_ADDRESSES [ next_program_index] ;
1362
+ let accounts = ( next_program_index..PROGRAM_ADDRESSES . len( ) )
1363
+ . map( |index| AccountMeta :: new_readonly( PROGRAM_ADDRESSES [ index] , false ) )
1364
+ . collect_vec( ) ;
1365
+ last_result = invoke_context. native_invoke(
1366
+ Instruction :: new_with_bytes(
1367
+ next_program_address,
1368
+ & [
1369
+ index_of_program_that_must_throw_exception as u8 ,
1370
+ next_program_index as u8 ,
1371
+ ] ,
1372
+ accounts,
1373
+ )
1374
+ . into( ) ,
1375
+ & [ ] ,
1376
+ ) ;
1377
+ if last_result. is_err( ) {
1378
+ return last_result;
1379
+ }
1380
+ }
1381
+ return last_result;
1382
+ }
1383
+
1384
+ Ok ( ( ) )
1385
+ } ) ;
1386
+
1387
+ // =======================================================
1388
+ // BEGIN: Create a transaction that calls the mock program
1389
+ // =======================================================
1390
+ let base_program_index = 0 ;
1391
+ let accounts = ( 0 ..PROGRAM_ADDRESSES . len ( ) )
1392
+ . map ( |index| AccountMeta :: new_readonly ( PROGRAM_ADDRESSES [ index] , false ) )
1393
+ . collect_vec ( ) ;
1394
+ let message: Message = Message :: new (
1395
+ & [ Instruction :: new_with_bytes (
1396
+ PROGRAM_ADDRESSES [ base_program_index] ,
1397
+ & [
1398
+ index_of_program_that_should_throw_exception,
1399
+ base_program_index as u8 , /* index of program being called */
1400
+ ] ,
1401
+ accounts,
1402
+ ) ] ,
1403
+ None ,
1404
+ ) ;
1405
+ let sanitized_message = new_unchecked_sanitized_message ( message) ;
1406
+ let mut program_cache_for_tx_batch = ProgramCacheForTxBatch :: default ( ) ;
1407
+ for index in 0 ..PROGRAM_ADDRESSES . len ( ) {
1408
+ program_cache_for_tx_batch. replenish (
1409
+ PROGRAM_ADDRESSES [ index] ,
1410
+ Arc :: new ( ProgramCacheEntry :: new_builtin ( 0 , 0 , MockBuiltin :: vm) ) ,
1411
+ ) ;
1412
+ }
1413
+ let batch_processor = TransactionBatchProcessor :: < TestForkGraph > :: default ( ) ;
1414
+ let sanitized_transaction = SanitizedTransaction :: new_for_tests (
1415
+ sanitized_message,
1416
+ vec ! [ Signature :: new_unique( ) ] ,
1417
+ false ,
1418
+ ) ;
1419
+ let mut mock_program_account = AccountSharedData :: new ( 1 , 0 , & native_loader:: id ( ) ) ;
1420
+ mock_program_account. set_executable ( true ) ;
1421
+ let loaded_transaction = LoadedTransaction {
1422
+ accounts : ( 0 ..PROGRAM_ADDRESSES . len ( ) )
1423
+ . map ( |index| ( PROGRAM_ADDRESSES [ index] , mock_program_account. clone ( ) ) )
1424
+ . collect_vec ( ) ,
1425
+ program_indices : vec ! [ vec![ 0 ] ] ,
1426
+ fee_details : FeeDetails :: default ( ) ,
1427
+ rollback_accounts : RollbackAccounts :: default ( ) ,
1428
+ compute_budget : SVMTransactionExecutionBudget :: default ( ) ,
1429
+ rent : 0 ,
1430
+ rent_debits : RentDebits :: default ( ) ,
1431
+ loaded_accounts_data_size : 32 ,
1432
+ } ;
1433
+ // =======================================================
1434
+ // END: Create a transaction that calls the mock program
1435
+ // =======================================================
1436
+
1437
+ let result = batch_processor. execute_loaded_transaction (
1438
+ & MockBankCallback :: default ( ) ,
1439
+ & sanitized_transaction,
1440
+ loaded_transaction,
1441
+ & mut ExecuteTimings :: default ( ) ,
1442
+ & mut TransactionErrorMetrics :: default ( ) ,
1443
+ & mut program_cache_for_tx_batch,
1444
+ & TransactionProcessingEnvironment :: default ( ) ,
1445
+ & TransactionProcessingConfig :: default ( ) ,
1446
+ ) ;
1447
+
1448
+ let status = result. execution_details . status ;
1449
+ assert_matches ! (
1450
+ status. err( ) ,
1451
+ Some ( TransactionError :: InstructionError (
1452
+ 0 ,
1453
+ InstructionError :: Custom ( 0xdeadbeef ) ,
1454
+ Some ( ii) ,
1455
+ ) ) if ii == index_of_program_that_should_throw_exception,
1456
+ "Expected the error to be attributable to the program with account index: \
1457
+ {index_of_program_that_should_throw_exception}."
1458
+ ) ;
1459
+ }
1460
+
1295
1461
#[ test]
1296
1462
fn test_execute_loaded_transaction_recordings ( ) {
1297
1463
// Setting all the arguments correctly is too burdensome for testing
0 commit comments