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 : }
|