LCOV - code coverage report
Current view: top level - flamenco/vm/syscall - fd_vm_syscall_pda.c (source / functions) Hit Total Coverage
Test: cov.lcov Lines: 135 146 92.5 %
Date: 2026-11-11 12:40:43 Functions: 4 4 100.0 %

          Line data    Source code
       1             : #include "fd_vm_syscall.h"
       2             : 
       3             : #include "../../../ballet/ed25519/fd_curve25519.h"
       4             : 
       5             : /* fd_compute_pda derives a PDA given:
       6             :    - the vm
       7             :    - the program id, which should be provided through either program_id or program_id_vaddr
       8             :       - This allows the user to pass in a program ID in either host address space or virtual address space.
       9             :       - If both are passed in, the host address space pubkey will be used.
      10             :    - the program_id pubkey in virtual address space. if the host address space pubkey is not given then the virtual address will be translated.
      11             :    - the seeds array vaddr
      12             :    - the seeds array count
      13             :    - an optional bump seed
      14             :    - out, the address in host address space where the PDA will be written to
      15             : 
      16             : If the derived PDA was not a valid ed25519 point, then this function will return FD_VM_SYSCALL_ERR_INVALID_PDA.
      17             : 
      18             : The derivation can also fail because of an out-of-bounds memory access, or an invalid seed list.
      19             :  */
      20             : int
      21             : fd_vm_derive_pda( fd_vm_t *           vm,
      22             :                   fd_pubkey_t const * program_id,
      23             :                   void const * *      seed_haddrs,
      24             :                   ulong *             seed_szs,
      25             :                   ulong               seeds_cnt,
      26             :                   uchar *             bump_seed,
      27        2016 :                   fd_pubkey_t *       out ) {
      28             : 
      29        2016 :   if( FD_UNLIKELY( seeds_cnt>FD_VM_PDA_SEEDS_MAX ) ) {
      30           0 :     FD_VM_ERR_FOR_LOG_SYSCALL( vm, FD_VM_SYSCALL_ERR_BAD_SEEDS );
      31           0 :     return FD_VM_SYSCALL_ERR_BAD_SEEDS;
      32           0 :   }
      33             : 
      34             :   /* This check does NOT halt execution within `fd_vm_syscall_sol_try_find_program_address`. This means
      35             :      that if the user provides 16 seeds (excluding the bump) in the `try_find_program_address` syscall,
      36             :      this same check below will be hit 255 times and deduct that many CUs. Very strange...
      37             :      https://github.com/anza-xyz/agave/blob/v2.1.0/sdk/pubkey/src/lib.rs#L725-L727 */
      38        2016 :   if( FD_UNLIKELY( seeds_cnt+( !!bump_seed )>FD_VM_PDA_SEEDS_MAX ) ) {
      39        1000 :     return FD_VM_SYSCALL_ERR_INVALID_PDA;
      40        1000 :   }
      41             : 
      42        5597 :   for( ulong i=0UL; i<seeds_cnt; i++ ) {
      43             :     /* This is an unconditional check in Agave:
      44             :        https://github.com/anza-xyz/agave/blob/v2.1.6/sdk/pubkey/src/lib.rs#L729-L731
      45             :      */
      46        4582 :     if( FD_UNLIKELY( seed_szs[ i ]>FD_VM_PDA_SEED_MEM_MAX ) ) {
      47           1 :       FD_VM_ERR_FOR_LOG_SYSCALL( vm, FD_VM_SYSCALL_ERR_BAD_SEEDS );
      48           1 :       return FD_VM_SYSCALL_ERR_BAD_SEEDS;
      49           1 :     }
      50        4582 :   }
      51             : 
      52        1015 :   fd_sha256_init( vm->sha );
      53        5593 :   for( ulong i=0UL; i<seeds_cnt; i++ ) {
      54        4578 :     ulong seed_sz = seed_szs[ i ];
      55             : 
      56             :     /* If the seed length is 0, then we don't need to append anything. solana_bpf_loader_program::syscalls::translate_slice
      57             :        returns an empty array in host space when given an empty array, which means this seed will have no affect on the PDA.
      58             :        https://github.com/anza-xyz/agave/blob/v2.1.0/programs/bpf_loader/src/syscalls/mod.rs#L737-L742 */
      59        4578 :     if( FD_UNLIKELY( !seed_sz ) ) {
      60         137 :       continue;
      61         137 :     }
      62        4441 :     void const * seed_haddr = seed_haddrs[ i ];
      63        4441 :     fd_sha256_append( vm->sha, seed_haddr, seed_sz );
      64        4441 :   }
      65             : 
      66             :   /* https://github.com/anza-xyz/agave/blob/v2.1.0/sdk/pubkey/src/lib.rs#L738-L747 */
      67        1015 :   if( bump_seed ) {
      68         562 :     fd_sha256_append( vm->sha, bump_seed, 1UL );
      69         562 :   }
      70             : 
      71        1015 :   if( FD_LIKELY( program_id )) {
      72        1015 :     fd_sha256_append( vm->sha, program_id, FD_PUBKEY_FOOTPRINT );
      73        1015 :   } else {
      74           0 :     FD_LOG_ERR(( "No program id passed in" ));
      75           0 :   }
      76             : 
      77        1015 :   fd_sha256_append( vm->sha, "ProgramDerivedAddress", 21UL ); /* TODO: use marker constant */
      78             : 
      79        1015 :   fd_sha256_fini( vm->sha, out );
      80             : 
      81             :   /* A PDA is valid if it is not a valid ed25519 curve point.
      82             :      In most cases the user will have derived the PDA off-chain, or the PDA is a known signer. */
      83        1015 :   if( FD_UNLIKELY( fd_ed25519_point_validate( out->key ) ) ) {
      84         409 :     return FD_VM_SYSCALL_ERR_INVALID_PDA;
      85         409 :   }
      86             : 
      87         606 :   return FD_VM_SUCCESS;
      88        1015 : }
      89             : 
      90             : /* fd_vm_translate_and_check_program_address_inputs is responsible for doing
      91             :    the preflight checks and translation of the seeds and program id.
      92             :    https://github.com/anza-xyz/agave/blob/v2.1.0/programs/bpf_loader/src/syscalls/mod.rs#L719 */
      93             : 
      94             : int
      95             : fd_vm_translate_and_check_program_address_inputs( fd_vm_t *             vm,
      96             :                                                   ulong                 seeds_vaddr,
      97             :                                                   ulong                 seeds_cnt,
      98             :                                                   ulong                 program_id_vaddr,
      99             :                                                   void const * *        out_seed_haddrs,
     100             :                                                   ulong *               out_seed_szs,
     101             :                                                   fd_pubkey_t const * * out_program_id,
     102         768 :                                                   uchar                 is_syscall ) {
     103             : 
     104        1522 :   fd_vm_vec_t const * untranslated_seeds = FD_VM_MEM_SLICE_HADDR_LD( vm, seeds_vaddr, FD_VM_ALIGN_RUST_SLICE_U8_REF,
     105        1522 :                                                                      fd_ulong_sat_mul( seeds_cnt, FD_VM_VEC_SIZE ) );
     106             : 
     107             :   /* This is a preflight check that is performed in Agave before deriving PDAs but after checking the seeds vaddr.
     108             :      When called to help CPI signer translation, this logs an
     109             :      instruction error:
     110             :      https://github.com/anza-xyz/agave/blob/v2.1.11/programs/bpf_loader/src/syscalls/cpi.rs#L538-L540
     111             :      However, when called from a syscall, this logs a syscall error:
     112             :      https://github.com/anza-xyz/agave/blob/v2.1.0/programs/bpf_loader/src/syscalls/mod.rs#L728-L730 */
     113         756 :   if( FD_UNLIKELY( seeds_cnt>FD_VM_PDA_SEEDS_MAX ) ) {
     114           2 :     if( is_syscall ) {
     115           1 :       FD_VM_ERR_FOR_LOG_SYSCALL( vm, FD_VM_SYSCALL_ERR_BAD_SEEDS );
     116           1 :       return FD_VM_SYSCALL_ERR_BAD_SEEDS;
     117           1 :     } else {
     118           1 :       FD_VM_ERR_FOR_LOG_INSTR( vm, FD_EXECUTOR_INSTR_ERR_MAX_SEED_LENGTH_EXCEEDED );
     119           1 :       return FD_EXECUTOR_INSTR_ERR_MAX_SEED_LENGTH_EXCEEDED;
     120           1 :     }
     121           2 :   }
     122        4006 :   for( ulong i=0UL; i<seeds_cnt; i++ ) {
     123        3257 :     ulong seed_sz = untranslated_seeds[i].len;
     124             :     /* Another preflight check
     125             :        https://github.com/anza-xyz/agave/blob/v2.1.0/programs/bpf_loader/src/syscalls/mod.rs#L734-L736
     126             :        When this function is called from syscalls, we would like to
     127             :        abort when exceeding SEED_MEM_MAX.
     128             :        However, when we reuse this function from CPI for signer
     129             :        translation, this check doesn't exist.  Sigh.
     130             :        Instead, the check is delayed until deriving PDA.
     131             :        https://github.com/anza-xyz/agave/blob/v2.1.6/programs/bpf_loader/src/syscalls/cpi.rs#L543
     132             :      */
     133        3257 :     if( FD_UNLIKELY( seed_sz>FD_VM_PDA_SEED_MEM_MAX && is_syscall ) ) {
     134           3 :       FD_VM_ERR_FOR_LOG_SYSCALL( vm, FD_VM_SYSCALL_ERR_BAD_SEEDS );
     135           3 :       return FD_VM_SYSCALL_ERR_BAD_SEEDS;
     136           3 :     }
     137        6506 :     void const * seed_haddr = FD_VM_MEM_SLICE_HADDR_LD( vm, untranslated_seeds[i].addr, FD_VM_ALIGN_RUST_U8, seed_sz );
     138           0 :     out_seed_haddrs[ i ] = seed_haddr;
     139        6506 :     out_seed_szs   [ i ] = seed_sz;
     140        6506 :   }
     141             : 
     142             :   /* We only want to do this check if the user requires it. */
     143         749 :   if( out_program_id ) {
     144        1312 :     *out_program_id = FD_VM_MEM_HADDR_LD( vm, program_id_vaddr, FD_VM_ALIGN_RUST_PUBKEY, FD_PUBKEY_FOOTPRINT );
     145        1312 :   }
     146         748 :   return FD_VM_SUCCESS;
     147         749 : }
     148             : 
     149             : /* fd_vm_syscall_sol_create_program_address is the entrypoint for the sol_create_program_address syscall:
     150             : https://github.com/anza-xyz/agave/blob/v2.0.8/programs/bpf_loader/src/syscalls/mod.rs#L729
     151             : 
     152             : The main semantic difference between Firedancer's implementation and Solana's is that Solana
     153             : translates all the seed pointers before doing any computation, while Firedancer translates
     154             : the seed pointers on-demand. This is to avoid an extra memory allocation.
     155             : 
     156             : This syscall creates a valid program derived address without searching for a bump seed.
     157             : It does this by hashing all the seeds, the program id, and the PDA marker, and then
     158             : checking if the resulting hash is a valid ed25519 curve point.
     159             : 
     160             : There is roughly a 50% chance of this syscall failing, due to the hash not being
     161             : a valid curve point, for any given collection of seeds.
     162             : 
     163             : Parameters:
     164             : - _vm: a pointer to the VM
     165             : - seed_vaddr: the address of the first element of an iovec-like scatter of a seed byte array in VM address space
     166             : - seed_cnt: the number of scatter elements
     167             : - program_id_vaddr: the address of the program id pubkey in VM address space
     168             : - out_vaddr: the address of the memory location where the resulting derived PDA will be written to, in VM address space, if the syscall is successful
     169             : - r5: unused
     170             : */
     171             : int
     172             : fd_vm_syscall_sol_create_program_address( /**/            void *  _vm,
     173             :                                           /**/            ulong   seeds_vaddr,
     174             :                                           /**/            ulong   seeds_cnt,
     175             :                                           /**/            ulong   program_id_vaddr,
     176             :                                           /**/            ulong   out_vaddr,
     177         149 :                                           FD_PARAM_UNUSED ulong   r5 )  {
     178         149 :   fd_vm_t * vm = (fd_vm_t *)_vm;
     179             : 
     180             : 
     181         149 :   FD_VM_CU_UPDATE( vm, FD_VM_CREATE_PROGRAM_ADDRESS_UNITS );
     182             : 
     183           0 :   void const *        seed_haddrs[ FD_VM_PDA_SEEDS_MAX ];
     184         149 :   ulong               seed_szs   [ FD_VM_PDA_SEEDS_MAX ];
     185         149 :   fd_pubkey_t const * program_id;
     186             : 
     187         149 :   int err = fd_vm_translate_and_check_program_address_inputs( vm,
     188         149 :                                                               seeds_vaddr,
     189         149 :                                                               seeds_cnt,
     190         149 :                                                               program_id_vaddr,
     191         149 :                                                               seed_haddrs,
     192         149 :                                                               seed_szs,
     193         149 :                                                               &program_id,
     194         149 :                                                               1U );
     195         149 :   if( FD_UNLIKELY( err ) ) {
     196           6 :     return err;
     197           6 :   }
     198             : 
     199         143 :   fd_pubkey_t derived[1];
     200         143 :   uchar *     bump_seed = NULL;
     201         143 :   err = fd_vm_derive_pda( vm, program_id, seed_haddrs, seed_szs, seeds_cnt, bump_seed, derived );
     202             :   /* Agave does their translation before the calculation, so if the translation fails we should fail
     203             :      the syscall.
     204             : 
     205             :      https://github.com/anza-xyz/agave/blob/v2.0.8/programs/bpf_loader/src/syscalls/mod.rs#L744-L750 */
     206         143 :   if ( FD_UNLIKELY( err != FD_VM_SUCCESS ) ) {
     207             : 
     208             :     /* Place 1 in r0 and successfully exit if we failed to derive a PDA
     209             :       https://github.com/anza-xyz/agave/blob/v2.0.8/programs/bpf_loader/src/syscalls/mod.rs#L753 */
     210          61 :     if ( FD_LIKELY( err == FD_VM_SYSCALL_ERR_INVALID_PDA ) ) {
     211          61 :       vm->reg[0] = 1UL;
     212          61 :       return FD_VM_SUCCESS;
     213          61 :     }
     214             : 
     215           0 :     return err;
     216          61 :   }
     217             : 
     218          82 :   uchar * address = FD_VM_HADDR_QUERY_U8_SLICE( vm, out_vaddr, FD_PUBKEY_FOOTPRINT );
     219          82 :   memcpy( address, derived->uc, FD_PUBKEY_FOOTPRINT );
     220             : 
     221             :   /* Success */
     222          82 :   vm->reg[0] = 0UL;
     223          82 :   return FD_VM_SUCCESS;
     224          82 : }
     225             : 
     226             : /* fd_vm_syscall_sol_try_find_program_address is the entrypoint for the sol_try_find_program_address syscall:
     227             : https://github.com/anza-xyz/agave/blob/v2.1.1/programs/bpf_loader/src/syscalls/mod.rs#L791
     228             : 
     229             : This syscall creates a valid program derived address, searching for a hash
     230             : that is not a valid ed25519 curve point by iterating through 255 possible
     231             : bump seeds.
     232             : 
     233             : It does this by hashing all the seeds, the program id, and the PDA marker, and then
     234             : checking if the resulting hash is a valid ed25519 curve point.
     235             :  */
     236             : int
     237             : fd_vm_syscall_sol_try_find_program_address( void *  _vm,
     238             :                                             ulong   seeds_vaddr,
     239             :                                             ulong   seeds_cnt,
     240             :                                             ulong   program_id_vaddr,
     241             :                                             ulong   out_vaddr,
     242         299 :                                             ulong   out_bump_seed_vaddr ) {
     243         299 :   fd_vm_t * vm = (fd_vm_t *)_vm;
     244             : 
     245             :   /* Costs the same as a create_program_address call.. weird but that is the protocol. */
     246         299 :   FD_VM_CU_UPDATE( vm, FD_VM_CREATE_PROGRAM_ADDRESS_UNITS );
     247             : 
     248             :   /* Similar to create_program_address but appends a 1 byte nonce that
     249             :      decrements from 255 down to 1 until a valid PDA is found.
     250             : 
     251             :      TODO: Solana Labs recomputes the SHA hash for each iteration here.  We
     252             :      can leverage SHA's streaming properties to precompute all but the last
     253             :      two blocks (1 data, 0 or 1 padding). PROBABLY NEED TO ADD CHECKPT / RESTORE
     254             :      CALLS TO SHA TO SUPPORT THIS)*/
     255             : 
     256           0 :   uchar bump_seed[1];
     257             : 
     258             :   /* First we need to do the preflight checks */
     259         299 :   void const *        seed_haddrs[ FD_VM_PDA_SEEDS_MAX ];
     260         299 :   ulong               seed_szs   [ FD_VM_PDA_SEEDS_MAX ];
     261         299 :   fd_pubkey_t const * program_id;
     262             : 
     263         299 :   int err = fd_vm_translate_and_check_program_address_inputs( vm,
     264         299 :                                                               seeds_vaddr,
     265         299 :                                                               seeds_cnt,
     266         299 :                                                               program_id_vaddr,
     267         299 :                                                               seed_haddrs,
     268         299 :                                                               seed_szs,
     269         299 :                                                               &program_id,
     270         299 :                                                               1U );
     271         299 :   if( FD_UNLIKELY( err ) ) {
     272           5 :     return err;
     273           5 :   }
     274             : 
     275        1565 :   for( ulong i=0UL; i<255UL; i++ ) {
     276        1562 :     bump_seed[0] = (uchar)(255UL - i);
     277             : 
     278        1562 :     fd_pubkey_t derived[1];
     279        1562 :     err = fd_vm_derive_pda( vm, program_id, seed_haddrs, seed_szs, seeds_cnt, bump_seed, derived );
     280             : 
     281             :     /* Stop looking if we have found a valid PDA */
     282        1562 :     if( FD_LIKELY( err==FD_VM_SUCCESS ) ) {
     283             : 
     284             :       /* https://github.com/anza-xyz/agave/blob/v2.3.1/programs/bpf_loader/src/syscalls/mod.rs#L919-L924 */
     285         288 :       fd_vm_haddr_query_t bump_seed_ref_query = {
     286         288 :         .vaddr    = out_bump_seed_vaddr,
     287         288 :         .align    = FD_VM_ALIGN_RUST_U8,
     288         288 :         .sz       = 1UL,
     289         288 :         .is_slice = 0,
     290         288 :       };
     291             : 
     292         288 :       fd_vm_haddr_query_t address_query = {
     293         288 :         .vaddr    = out_vaddr,
     294         288 :         .align    = FD_VM_ALIGN_RUST_U8,
     295         288 :         .sz       = FD_PUBKEY_FOOTPRINT,
     296         288 :         .is_slice = 1,
     297         288 :       };
     298             : 
     299         288 :       fd_vm_haddr_query_t * queries[] = { &bump_seed_ref_query, &address_query };
     300         288 :       FD_VM_TRANSLATE_MUT( vm, queries );
     301             : 
     302         284 :       memcpy( address_query.haddr, derived->uc, sizeof(fd_pubkey_t) );
     303         284 :       memcpy( bump_seed_ref_query.haddr, bump_seed, sizeof(uchar) );
     304             : 
     305         284 :       vm->reg[0] = 0UL;
     306         284 :       return FD_VM_SUCCESS;
     307        1274 :     } else if( FD_UNLIKELY( err!=FD_VM_SYSCALL_ERR_INVALID_PDA ) ) {
     308           0 :       return err;
     309           0 :     }
     310             : 
     311        2545 :     FD_VM_CU_UPDATE( vm, FD_VM_CREATE_PROGRAM_ADDRESS_UNITS );
     312        2545 :   }
     313             : 
     314           3 :   vm->reg[0] = 1UL;
     315           3 :   return FD_VM_SUCCESS;
     316         294 : }

Generated by: LCOV version 1.14