Skip to main content

hopper_runtime/
crypto.rs

1//! Runtime cryptography helpers built on Solana syscalls and precompiles.
2
3use crate::{Address, ProgramError};
4
5pub type Sha256Hash = [u8; 32];
6pub type Keccak256Hash = [u8; 32];
7pub type Blake3Hash = [u8; 32];
8pub type Secp256k1Pubkey = [u8; 64];
9pub type EthereumAddress = [u8; 20];
10pub type CurvePoint = [u8; 32];
11pub type CurveScalar = [u8; 32];
12pub type PoseidonHash = [u8; 32];
13pub type AltBn128G1 = [u8; 64];
14pub type AltBn128G1Compressed = [u8; 32];
15pub type AltBn128G2 = [u8; 128];
16pub type AltBn128G2Compressed = [u8; 64];
17pub type AltBn128PairingResult = [u8; 32];
18
19pub const MAX_HASH_SEGMENTS: usize = 16;
20pub const CURVE25519_EDWARDS: u64 = 0;
21pub const CURVE25519_RISTRETTO: u64 = 1;
22pub const CURVE_GROUP_ADD: u64 = 0;
23pub const CURVE_GROUP_SUB: u64 = 1;
24pub const CURVE_GROUP_MUL: u64 = 2;
25pub const POSEIDON_BN254_X5: u64 = 0;
26pub const POSEIDON_BIG_ENDIAN: u64 = 0;
27pub const POSEIDON_LITTLE_ENDIAN: u64 = 1;
28pub const MAX_POSEIDON_INPUTS: usize = 12;
29pub const POSEIDON_INPUT_LEN: usize = 32;
30pub const ALT_BN128_LE_FLAG: u64 = 0x80;
31pub const ALT_BN128_FIELD_SIZE: usize = 32;
32pub const ALT_BN128_G1_POINT_SIZE: usize = 64;
33pub const ALT_BN128_G2_POINT_SIZE: usize = 128;
34pub const ALT_BN128_G1_ADDITION_INPUT_SIZE: usize = 128;
35pub const ALT_BN128_G2_ADDITION_INPUT_SIZE: usize = 256;
36pub const ALT_BN128_G1_MULTIPLICATION_INPUT_SIZE: usize = 96;
37pub const ALT_BN128_G2_MULTIPLICATION_INPUT_SIZE: usize = 160;
38pub const ALT_BN128_PAIRING_ELEMENT_SIZE: usize = 192;
39pub const ALT_BN128_G1_ADD_BE: u64 = 0;
40pub const ALT_BN128_G1_SUB_BE: u64 = 1;
41pub const ALT_BN128_G1_MUL_BE: u64 = 2;
42pub const ALT_BN128_PAIRING_BE: u64 = 3;
43pub const ALT_BN128_G2_ADD_BE: u64 = 4;
44pub const ALT_BN128_G2_SUB_BE: u64 = 5;
45pub const ALT_BN128_G2_MUL_BE: u64 = 6;
46pub const ALT_BN128_G1_ADD_LE: u64 = ALT_BN128_G1_ADD_BE | ALT_BN128_LE_FLAG;
47pub const ALT_BN128_G1_SUB_LE: u64 = ALT_BN128_G1_SUB_BE | ALT_BN128_LE_FLAG;
48pub const ALT_BN128_G1_MUL_LE: u64 = ALT_BN128_G1_MUL_BE | ALT_BN128_LE_FLAG;
49pub const ALT_BN128_PAIRING_LE: u64 = ALT_BN128_PAIRING_BE | ALT_BN128_LE_FLAG;
50pub const ALT_BN128_G2_ADD_LE: u64 = ALT_BN128_G2_ADD_BE | ALT_BN128_LE_FLAG;
51pub const ALT_BN128_G2_SUB_LE: u64 = ALT_BN128_G2_SUB_BE | ALT_BN128_LE_FLAG;
52pub const ALT_BN128_G2_MUL_LE: u64 = ALT_BN128_G2_MUL_BE | ALT_BN128_LE_FLAG;
53pub const ALT_BN128_G1_COMPRESS_BE: u64 = 0;
54pub const ALT_BN128_G1_DECOMPRESS_BE: u64 = 1;
55pub const ALT_BN128_G2_COMPRESS_BE: u64 = 2;
56pub const ALT_BN128_G2_DECOMPRESS_BE: u64 = 3;
57pub const ALT_BN128_G1_COMPRESS_LE: u64 = ALT_BN128_G1_COMPRESS_BE | ALT_BN128_LE_FLAG;
58pub const ALT_BN128_G1_DECOMPRESS_LE: u64 = ALT_BN128_G1_DECOMPRESS_BE | ALT_BN128_LE_FLAG;
59pub const ALT_BN128_G2_COMPRESS_LE: u64 = ALT_BN128_G2_COMPRESS_BE | ALT_BN128_LE_FLAG;
60pub const ALT_BN128_G2_DECOMPRESS_LE: u64 = ALT_BN128_G2_DECOMPRESS_BE | ALT_BN128_LE_FLAG;
61pub const MAX_INSTRUCTION_DATA_LEN: usize = 1232;
62pub const MAX_INSTRUCTION_ACCOUNTS_BYTES: usize = 2176;
63
64pub const ED25519_PROGRAM_ID: Address = Address::new_from_array(crate::__decode_base58_32(
65    "Ed25519SigVerify111111111111111111111111111",
66));
67
68pub const SECP256K1_PROGRAM_ID: Address = Address::new_from_array(crate::__decode_base58_32(
69    "KeccakSecp256k11111111111111111111111111111",
70));
71
72/// Secp256r1 (P-256) signature-verification precompile (SIMD-0075).
73/// This is the precompile passkey / WebAuthn flows verify against.
74pub const SECP256R1_PROGRAM_ID: Address = Address::new_from_array(crate::__decode_base58_32(
75    "Secp256r1SigVerify1111111111111111111111111",
76));
77
78#[derive(Clone, Debug)]
79pub struct ProcessedInstruction {
80    pub program_id: Address,
81    pub data: [u8; MAX_INSTRUCTION_DATA_LEN],
82    pub data_len: usize,
83    pub accounts_len: usize,
84}
85
86#[derive(Clone, Debug)]
87pub struct ProcessedInstructionData<const MAX_DATA: usize> {
88    pub program_id: Address,
89    pub data: [u8; MAX_DATA],
90    pub data_len: usize,
91}
92
93/// Native account-meta ABI used by caller-buffer sibling introspection.
94pub use hopper_native::introspect::ProcessedInstructionAccount;
95
96/// Processed sibling with runtime address and borrowed scratch prefixes.
97#[derive(Debug)]
98pub struct ProcessedInstructionView<'a> {
99    pub program_id: Address,
100    pub data: &'a [u8],
101    pub accounts: &'a [ProcessedInstructionAccount],
102}
103
104#[cfg(feature = "crypto-big-mod-exp")]
105#[repr(C)]
106struct BigModExpParams {
107    base: *const u8,
108    base_len: u64,
109    exponent: *const u8,
110    exponent_len: u64,
111    modulus: *const u8,
112    modulus_len: u64,
113}
114
115#[cfg(any(
116    feature = "crypto-curve",
117    feature = "crypto-poseidon",
118    feature = "crypto-bn254",
119    feature = "crypto-big-mod-exp"
120))]
121#[inline]
122fn syscall_error(status: u64) -> ProgramError {
123    if status <= u32::MAX as u64 {
124        ProgramError::Custom(status as u32)
125    } else {
126        ProgramError::InvalidArgument
127    }
128}
129
130#[inline]
131pub fn sha256(inputs: &[&[u8]]) -> Result<Sha256Hash, ProgramError> {
132    if inputs.len() > MAX_HASH_SEGMENTS {
133        return Err(ProgramError::InvalidArgument);
134    }
135
136    let mut result = [0u8; 32];
137    // SAFETY: `inputs` is a valid slice of slice descriptors and `result`
138    // points to exactly 32 writable output bytes.
139    unsafe {
140        crate::syscalls::sol_sha256(
141            inputs as *const _ as *const u8,
142            inputs.len() as u64,
143            result.as_mut_ptr(),
144        );
145    }
146    Ok(result)
147}
148
149#[inline]
150pub fn sha256_single(input: &[u8]) -> Result<Sha256Hash, ProgramError> {
151    sha256(&[input])
152}
153
154#[inline]
155pub fn keccak256(inputs: &[&[u8]]) -> Result<Keccak256Hash, ProgramError> {
156    if inputs.len() > MAX_HASH_SEGMENTS {
157        return Err(ProgramError::InvalidArgument);
158    }
159
160    let mut result = [0u8; 32];
161    // SAFETY: `inputs` is a valid slice of slice descriptors and `result`
162    // points to exactly 32 writable output bytes.
163    unsafe {
164        crate::syscalls::sol_keccak256(
165            inputs as *const _ as *const u8,
166            inputs.len() as u64,
167            result.as_mut_ptr(),
168        );
169    }
170    Ok(result)
171}
172
173#[inline]
174pub fn keccak256_single(input: &[u8]) -> Result<Keccak256Hash, ProgramError> {
175    keccak256(&[input])
176}
177
178#[inline]
179pub fn blake3(inputs: &[&[u8]]) -> Result<Blake3Hash, ProgramError> {
180    if inputs.len() > MAX_HASH_SEGMENTS {
181        return Err(ProgramError::InvalidArgument);
182    }
183
184    let mut result = [0u8; 32];
185    // SAFETY: `inputs` is a valid slice of slice descriptors and `result`
186    // points to exactly 32 writable output bytes.
187    let rc = unsafe {
188        crate::syscalls::sol_blake3(
189            inputs as *const _ as *const u8,
190            inputs.len() as u64,
191            result.as_mut_ptr(),
192        )
193    };
194    if rc != 0 {
195        return Err(ProgramError::InvalidArgument);
196    }
197    Ok(result)
198}
199
200#[inline]
201pub fn blake3_single(input: &[u8]) -> Result<Blake3Hash, ProgramError> {
202    blake3(&[input])
203}
204
205#[inline]
206pub fn secp256k1_recover(
207    message_hash: &[u8; 32],
208    recovery_id: u8,
209    signature: &[u8; 64],
210) -> Result<Secp256k1Pubkey, ProgramError> {
211    let mut result = [0u8; 64];
212    // SAFETY: all pointers refer to fixed-width buffers required by the
213    // Solana secp256k1 recover syscall.
214    let rc = unsafe {
215        crate::syscalls::sol_secp256k1_recover(
216            message_hash.as_ptr(),
217            recovery_id as u64,
218            signature.as_ptr(),
219            result.as_mut_ptr(),
220        )
221    };
222    if rc != 0 {
223        return Err(ProgramError::InvalidArgument);
224    }
225    Ok(result)
226}
227
228#[inline]
229pub fn recover_ethereum_address(
230    message_hash: &[u8; 32],
231    recovery_id: u8,
232    signature: &[u8; 64],
233) -> Result<EthereumAddress, ProgramError> {
234    let pubkey = secp256k1_recover(message_hash, recovery_id, signature)?;
235    let digest = keccak256(&[&pubkey])?;
236    let mut address = [0u8; 20];
237    address.copy_from_slice(&digest[12..32]);
238    Ok(address)
239}
240
241/// Whether `point` is a valid encoding of a point on the curve.
242///
243/// In a program this is the curve syscall. On the host the Edwards
244/// answer is computed in Rust, by the same check that decides program
245/// derived addresses, so a unit test sees what the cluster would say.
246/// Ristretto has no host implementation and answers `false` there.
247#[inline]
248pub fn curve_validate_point(curve_id: u64, point: &[u8; 32]) -> Result<bool, ProgramError> {
249    #[cfg(not(target_os = "solana"))]
250    if curve_id == CURVE25519_EDWARDS {
251        return Ok(hopper_native::curve25519::is_on_curve(point));
252    }
253    // SAFETY: `point` points to exactly 32 bytes; null output pointer requests
254    // validation-only behavior from Solana's curve syscall.
255    let rc = unsafe {
256        crate::syscalls::sol_curve_validate_point(curve_id, point.as_ptr(), core::ptr::null_mut())
257    };
258    Ok(rc == 0)
259}
260
261#[inline]
262pub fn curve25519_edwards_validate_point(point: &[u8; 32]) -> Result<bool, ProgramError> {
263    curve_validate_point(CURVE25519_EDWARDS, point)
264}
265
266#[cfg(feature = "crypto-curve")]
267#[inline]
268fn curve_group_op(
269    curve_id: u64,
270    group_op: u64,
271    left: &CurvePoint,
272    right: &CurvePoint,
273) -> Result<CurvePoint, ProgramError> {
274    let mut result = [0u8; 32];
275    // SAFETY: all operands and the output are fixed-width 32-byte curve buffers.
276    let rc = unsafe {
277        crate::syscalls::sol_curve_group_op(
278            curve_id,
279            group_op,
280            left.as_ptr(),
281            right.as_ptr(),
282            result.as_mut_ptr(),
283        )
284    };
285    if rc != 0 {
286        return Err(syscall_error(rc));
287    }
288    Ok(result)
289}
290
291#[cfg(feature = "crypto-curve")]
292#[inline]
293pub fn curve_group_add(
294    curve_id: u64,
295    left: &CurvePoint,
296    right: &CurvePoint,
297) -> Result<CurvePoint, ProgramError> {
298    curve_group_op(curve_id, CURVE_GROUP_ADD, left, right)
299}
300
301#[cfg(feature = "crypto-curve")]
302#[inline]
303pub fn curve_group_sub(
304    curve_id: u64,
305    left: &CurvePoint,
306    right: &CurvePoint,
307) -> Result<CurvePoint, ProgramError> {
308    curve_group_op(curve_id, CURVE_GROUP_SUB, left, right)
309}
310
311#[cfg(feature = "crypto-curve")]
312#[inline]
313pub fn curve_group_mul(
314    curve_id: u64,
315    scalar: &CurveScalar,
316    point: &CurvePoint,
317) -> Result<CurvePoint, ProgramError> {
318    let mut result = [0u8; 32];
319    // SAFETY: Solana's multiply op expects scalar left, point right, and a
320    // fixed-width 32-byte result buffer.
321    let rc = unsafe {
322        crate::syscalls::sol_curve_group_op(
323            curve_id,
324            CURVE_GROUP_MUL,
325            scalar.as_ptr(),
326            point.as_ptr(),
327            result.as_mut_ptr(),
328        )
329    };
330    if rc != 0 {
331        return Err(syscall_error(rc));
332    }
333    Ok(result)
334}
335
336#[cfg(feature = "crypto-curve")]
337#[inline]
338pub fn curve_multiscalar_mul(
339    curve_id: u64,
340    scalars: &[CurveScalar],
341    points: &[CurvePoint],
342) -> Result<CurvePoint, ProgramError> {
343    if scalars.len() != points.len() || points.is_empty() {
344        return Err(ProgramError::InvalidArgument);
345    }
346
347    let mut result = [0u8; 32];
348    // SAFETY: `scalars` and `points` are contiguous arrays of 32-byte encodings
349    // with matching lengths, and `result` is a 32-byte output buffer.
350    let rc = unsafe {
351        crate::syscalls::sol_curve_multiscalar_mul(
352            curve_id,
353            scalars.as_ptr() as *const u8,
354            points.as_ptr() as *const u8,
355            points.len() as u64,
356            result.as_mut_ptr(),
357        )
358    };
359    if rc != 0 {
360        return Err(syscall_error(rc));
361    }
362    Ok(result)
363}
364
365#[cfg(feature = "crypto-poseidon")]
366#[inline]
367pub fn poseidon_hashv(
368    parameters: u64,
369    endianness: u64,
370    inputs: &[&[u8]],
371) -> Result<PoseidonHash, ProgramError> {
372    if inputs.is_empty() || inputs.len() > MAX_POSEIDON_INPUTS {
373        return Err(ProgramError::InvalidArgument);
374    }
375    if inputs.iter().any(|input| input.len() != POSEIDON_INPUT_LEN) {
376        return Err(ProgramError::InvalidArgument);
377    }
378
379    let mut result = [0u8; 32];
380    // SAFETY: `inputs` is a valid slice-descriptor array and `result` points to
381    // exactly 32 writable bytes.
382    let rc = unsafe {
383        crate::syscalls::sol_poseidon(
384            parameters,
385            endianness,
386            inputs as *const _ as *const u8,
387            inputs.len() as u64,
388            result.as_mut_ptr(),
389        )
390    };
391    if rc != 0 {
392        return Err(syscall_error(rc));
393    }
394    Ok(result)
395}
396
397#[cfg(feature = "crypto-poseidon")]
398#[inline]
399pub fn poseidon_hash(
400    parameters: u64,
401    endianness: u64,
402    input: &[u8; 32],
403) -> Result<PoseidonHash, ProgramError> {
404    poseidon_hashv(parameters, endianness, &[input])
405}
406
407#[cfg(feature = "crypto-poseidon")]
408#[inline]
409pub fn poseidon_bn254_x5(inputs: &[&[u8]]) -> Result<PoseidonHash, ProgramError> {
410    poseidon_hashv(POSEIDON_BN254_X5, POSEIDON_BIG_ENDIAN, inputs)
411}
412
413#[cfg(feature = "crypto-bn254")]
414#[inline]
415fn alt_bn128_group_op<const OUT: usize>(
416    group_op: u64,
417    input: &[u8],
418) -> Result<[u8; OUT], ProgramError> {
419    let mut result = [0u8; OUT];
420    // SAFETY: callers choose `OUT` to match the selected BN254 syscall op.
421    let rc = unsafe {
422        crate::syscalls::sol_alt_bn128_group_op(
423            group_op,
424            input.as_ptr(),
425            input.len() as u64,
426            result.as_mut_ptr(),
427        )
428    };
429    if rc != 0 {
430        return Err(syscall_error(rc));
431    }
432    Ok(result)
433}
434
435#[cfg(feature = "crypto-bn254")]
436#[inline]
437fn alt_bn128_compression_op<const OUT: usize>(
438    op: u64,
439    input: &[u8],
440) -> Result<[u8; OUT], ProgramError> {
441    let mut result = [0u8; OUT];
442    // SAFETY: callers choose `OUT` to match the selected BN254 compression op.
443    let rc = unsafe {
444        crate::syscalls::sol_alt_bn128_compression(
445            op,
446            input.as_ptr(),
447            input.len() as u64,
448            result.as_mut_ptr(),
449        )
450    };
451    if rc != 0 {
452        return Err(syscall_error(rc));
453    }
454    Ok(result)
455}
456
457#[cfg(feature = "crypto-bn254")]
458#[inline]
459pub fn alt_bn128_g1_addition_be(input: &[u8]) -> Result<AltBn128G1, ProgramError> {
460    if input.len() > ALT_BN128_G1_ADDITION_INPUT_SIZE {
461        return Err(ProgramError::InvalidArgument);
462    }
463    alt_bn128_group_op::<ALT_BN128_G1_POINT_SIZE>(ALT_BN128_G1_ADD_BE, input)
464}
465
466#[cfg(feature = "crypto-bn254")]
467#[inline]
468pub fn alt_bn128_g1_multiplication_be(input: &[u8]) -> Result<AltBn128G1, ProgramError> {
469    if input.len() > ALT_BN128_G1_MULTIPLICATION_INPUT_SIZE {
470        return Err(ProgramError::InvalidArgument);
471    }
472    alt_bn128_group_op::<ALT_BN128_G1_POINT_SIZE>(ALT_BN128_G1_MUL_BE, input)
473}
474
475#[cfg(feature = "crypto-bn254")]
476#[inline]
477pub fn alt_bn128_pairing_be(input: &[u8]) -> Result<AltBn128PairingResult, ProgramError> {
478    if !input.len().is_multiple_of(ALT_BN128_PAIRING_ELEMENT_SIZE) {
479        return Err(ProgramError::InvalidArgument);
480    }
481    alt_bn128_group_op::<ALT_BN128_FIELD_SIZE>(ALT_BN128_PAIRING_BE, input)
482}
483
484#[cfg(feature = "crypto-bn254")]
485#[inline]
486pub fn alt_bn128_add(input: &[u8]) -> Result<AltBn128G1, ProgramError> {
487    alt_bn128_g1_addition_be(input)
488}
489
490#[cfg(feature = "crypto-bn254")]
491#[inline]
492pub fn alt_bn128_mul(input: &[u8]) -> Result<AltBn128G1, ProgramError> {
493    alt_bn128_g1_multiplication_be(input)
494}
495
496#[cfg(feature = "crypto-bn254")]
497#[inline]
498pub fn alt_bn128_pairing(input: &[u8]) -> Result<AltBn128PairingResult, ProgramError> {
499    alt_bn128_pairing_be(input)
500}
501
502#[cfg(feature = "crypto-bn254")]
503#[inline]
504pub fn alt_bn128_g1_compress_be(input: &AltBn128G1) -> Result<AltBn128G1Compressed, ProgramError> {
505    alt_bn128_compression_op::<ALT_BN128_FIELD_SIZE>(ALT_BN128_G1_COMPRESS_BE, input)
506}
507
508#[cfg(feature = "crypto-bn254")]
509#[inline]
510pub fn alt_bn128_g1_decompress_be(
511    input: &AltBn128G1Compressed,
512) -> Result<AltBn128G1, ProgramError> {
513    alt_bn128_compression_op::<ALT_BN128_G1_POINT_SIZE>(ALT_BN128_G1_DECOMPRESS_BE, input)
514}
515
516#[cfg(feature = "crypto-bn254")]
517#[inline]
518pub fn alt_bn128_g2_compress_be(input: &AltBn128G2) -> Result<AltBn128G2Compressed, ProgramError> {
519    alt_bn128_compression_op::<ALT_BN128_G1_POINT_SIZE>(ALT_BN128_G2_COMPRESS_BE, input)
520}
521
522#[cfg(feature = "crypto-bn254")]
523#[inline]
524pub fn alt_bn128_g2_decompress_be(
525    input: &AltBn128G2Compressed,
526) -> Result<AltBn128G2, ProgramError> {
527    alt_bn128_compression_op::<ALT_BN128_G2_POINT_SIZE>(ALT_BN128_G2_DECOMPRESS_BE, input)
528}
529
530#[cfg(feature = "crypto-big-mod-exp")]
531#[inline]
532pub fn big_mod_exp(
533    base: &[u8],
534    exponent: &[u8],
535    modulus: &[u8],
536    output: &mut [u8],
537) -> Result<(), ProgramError> {
538    if modulus.is_empty() || output.len() != modulus.len() {
539        return Err(ProgramError::InvalidArgument);
540    }
541
542    let params = BigModExpParams {
543        base: base.as_ptr(),
544        base_len: base.len() as u64,
545        exponent: exponent.as_ptr(),
546        exponent_len: exponent.len() as u64,
547        modulus: modulus.as_ptr(),
548        modulus_len: modulus.len() as u64,
549    };
550    // SAFETY: params has Solana's C layout and `output` has exactly modulus.len()
551    // writable bytes, which is the syscall's output size contract.
552    let rc = unsafe {
553        crate::syscalls::sol_big_mod_exp(
554            &params as *const BigModExpParams as *const u8,
555            output.as_mut_ptr(),
556        )
557    };
558    if rc != 0 {
559        return Err(syscall_error(rc));
560    }
561    Ok(())
562}
563
564#[inline(always)]
565pub fn get_stack_height() -> u64 {
566    crate::syscalls::sol_get_stack_height()
567}
568
569#[inline(always)]
570pub fn is_top_level() -> bool {
571    get_stack_height() <= 1
572}
573
574#[inline(always)]
575pub fn is_cpi() -> bool {
576    get_stack_height() > 1
577}
578
579#[inline(always)]
580pub fn require_top_level() -> Result<(), ProgramError> {
581    if is_top_level() {
582        Ok(())
583    } else {
584        Err(ProgramError::InvalidArgument)
585    }
586}
587
588/// Read a sibling into caller-provided buffers, preserving exact runtime sizes.
589/// See [`hopper_native::introspect::get_processed_instruction_into`] for scope,
590/// capacity errors and host behavior. Account records use native address values;
591/// convert an individual address with `Address::from(record.address.clone())`.
592#[inline]
593pub fn get_processed_instruction_into<'a>(
594    index: u64,
595    data: &'a mut [u8],
596    accounts: &'a mut [ProcessedInstructionAccount],
597) -> Result<Option<ProcessedInstructionView<'a>>, ProgramError> {
598    hopper_native::introspect::get_processed_instruction_into(index, data, accounts)
599        .map(|view| {
600            view.map(|view| ProcessedInstructionView {
601                program_id: view.program_id.into(),
602                data: view.data,
603                accounts: view.accounts,
604            })
605        })
606        .map_err(ProgramError::from)
607}
608
609/// Read up to 1,232 bytes and 64 account metas. `None` means missing or too large.
610/// Use [`get_processed_instruction_into`] to distinguish these cases.
611#[inline]
612pub fn get_processed_instruction(index: u64) -> Option<ProcessedInstruction> {
613    let view = hopper_native::introspect::get_processed_instruction(index)?;
614    Some(ProcessedInstruction {
615        program_id: view.program_id.into(),
616        data: view.data,
617        data_len: view.data_len,
618        accounts_len: view.accounts_len,
619    })
620}
621
622/// Read bounded instruction data from a sibling with up to 64 account metas.
623/// The entire data must fit `MAX_DATA`; this never returns truncated data.
624/// Account scratch is still required by the syscall even when the caller only
625/// wants data. Use [`get_processed_instruction_into`] to select both capacities.
626#[inline]
627pub fn get_processed_instruction_data<const MAX_DATA: usize>(
628    index: u64,
629) -> Option<ProcessedInstructionData<MAX_DATA>> {
630    let mut data = [0; MAX_DATA];
631    let mut accounts = core::array::from_fn::<_, 64, _>(|_| ProcessedInstructionAccount::default());
632    let view = get_processed_instruction_into(index, &mut data, &mut accounts).ok()??;
633    let program_id = view.program_id;
634    let data_len = view.data.len();
635    Some(ProcessedInstructionData {
636        program_id,
637        data,
638        data_len,
639    })
640}
641
642/// Checks only the sibling's program ID. The caller must validate the signature
643/// count, offsets, referenced instruction bytes and expected key/message.
644#[inline]
645pub fn require_ed25519_instruction(
646    sibling_index: u64,
647) -> Result<ProcessedInstruction, ProgramError> {
648    let instruction =
649        get_processed_instruction(sibling_index).ok_or(ProgramError::InvalidArgument)?;
650    if instruction.program_id != ED25519_PROGRAM_ID {
651        return Err(ProgramError::IncorrectProgramId);
652    }
653    Ok(instruction)
654}
655
656/// Bounded variant of [`require_ed25519_instruction`], with the same payload checks
657/// required of the caller.
658#[inline]
659pub fn require_ed25519_instruction_data<const MAX_DATA: usize>(
660    sibling_index: u64,
661) -> Result<ProcessedInstructionData<MAX_DATA>, ProgramError> {
662    let instruction =
663        get_processed_instruction_data(sibling_index).ok_or(ProgramError::InvalidArgument)?;
664    if instruction.program_id != ED25519_PROGRAM_ID {
665        return Err(ProgramError::IncorrectProgramId);
666    }
667    Ok(instruction)
668}
669
670/// Checks only the sibling's program ID, not signature payload or authorization.
671#[inline]
672pub fn require_secp256k1_instruction(
673    sibling_index: u64,
674) -> Result<ProcessedInstruction, ProgramError> {
675    let instruction =
676        get_processed_instruction(sibling_index).ok_or(ProgramError::InvalidArgument)?;
677    if instruction.program_id != SECP256K1_PROGRAM_ID {
678        return Err(ProgramError::IncorrectProgramId);
679    }
680    Ok(instruction)
681}
682
683/// Require that a sibling instruction targeted the Secp256r1 (P-256)
684/// precompile. Checks only the program ID. The caller validates signature count,
685/// offsets, expected key/message, and any WebAuthn challenge and relying-party policy.
686#[inline]
687pub fn require_secp256r1_instruction(
688    sibling_index: u64,
689) -> Result<ProcessedInstruction, ProgramError> {
690    let instruction =
691        get_processed_instruction(sibling_index).ok_or(ProgramError::InvalidArgument)?;
692    if instruction.program_id != SECP256R1_PROGRAM_ID {
693        return Err(ProgramError::IncorrectProgramId);
694    }
695    Ok(instruction)
696}
697
698/// Variant of [`require_secp256r1_instruction`] returning the bounded
699/// instruction-data form for callers that parse the precompile payload.
700#[inline]
701pub fn require_secp256r1_instruction_data<const MAX_DATA: usize>(
702    sibling_index: u64,
703) -> Result<ProcessedInstructionData<MAX_DATA>, ProgramError> {
704    let instruction =
705        get_processed_instruction_data(sibling_index).ok_or(ProgramError::InvalidArgument)?;
706    if instruction.program_id != SECP256R1_PROGRAM_ID {
707        return Err(ProgramError::IncorrectProgramId);
708    }
709    Ok(instruction)
710}
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715
716    const EMPTY: &[u8] = b"";
717
718    #[test]
719    fn host_without_a_trace_has_no_processed_siblings() {
720        assert!(get_processed_instruction(0).is_none());
721        assert!(get_processed_instruction_data::<16>(0).is_none());
722        assert!(get_processed_instruction_into(0, &mut [], &mut [])
723            .unwrap()
724            .is_none());
725        assert_eq!(
726            require_ed25519_instruction(0).unwrap_err(),
727            ProgramError::InvalidArgument
728        );
729    }
730
731    #[test]
732    fn hash_helpers_accept_sixteen_segments() {
733        let inputs = [EMPTY; MAX_HASH_SEGMENTS];
734
735        assert!(sha256(&inputs).is_ok());
736        assert!(keccak256(&inputs).is_ok());
737        assert!(blake3(&inputs).is_ok());
738    }
739
740    #[test]
741    fn hash_helpers_reject_more_than_sixteen_segments() {
742        let inputs = [EMPTY; MAX_HASH_SEGMENTS + 1];
743
744        assert_eq!(sha256(&inputs), Err(ProgramError::InvalidArgument));
745        assert_eq!(keccak256(&inputs), Err(ProgramError::InvalidArgument));
746        assert_eq!(blake3(&inputs), Err(ProgramError::InvalidArgument));
747    }
748
749    #[cfg(feature = "crypto-curve")]
750    #[test]
751    fn curve_msm_requires_matching_nonempty_inputs() {
752        let scalars = [[0u8; 32]; 2];
753        let points = [[0u8; 32]; 1];
754
755        assert_eq!(
756            curve_multiscalar_mul(CURVE25519_EDWARDS, &scalars, &points),
757            Err(ProgramError::InvalidArgument)
758        );
759        assert_eq!(
760            curve_multiscalar_mul(CURVE25519_EDWARDS, &[], &[]),
761            Err(ProgramError::InvalidArgument)
762        );
763    }
764
765    #[cfg(feature = "crypto-poseidon")]
766    #[test]
767    fn poseidon_rejects_bad_input_shape() {
768        let short = [0u8; 31];
769        let input = [0u8; 32];
770        let too_many = [&input[..]; MAX_POSEIDON_INPUTS + 1];
771
772        assert_eq!(
773            poseidon_hashv(POSEIDON_BN254_X5, POSEIDON_BIG_ENDIAN, &[]),
774            Err(ProgramError::InvalidArgument)
775        );
776        assert_eq!(
777            poseidon_hashv(POSEIDON_BN254_X5, POSEIDON_BIG_ENDIAN, &[&short]),
778            Err(ProgramError::InvalidArgument)
779        );
780        assert_eq!(
781            poseidon_hashv(POSEIDON_BN254_X5, POSEIDON_BIG_ENDIAN, &too_many),
782            Err(ProgramError::InvalidArgument)
783        );
784    }
785
786    #[cfg(feature = "crypto-bn254")]
787    #[test]
788    fn bn254_rejects_bad_input_lengths() {
789        let oversized_add = [0u8; ALT_BN128_G1_ADDITION_INPUT_SIZE + 1];
790        let bad_pairing = [0u8; ALT_BN128_PAIRING_ELEMENT_SIZE + 1];
791
792        assert_eq!(
793            alt_bn128_g1_addition_be(&oversized_add),
794            Err(ProgramError::InvalidArgument)
795        );
796        assert_eq!(
797            alt_bn128_pairing_be(&bad_pairing),
798            Err(ProgramError::InvalidArgument)
799        );
800    }
801
802    #[cfg(feature = "crypto-big-mod-exp")]
803    #[test]
804    fn big_mod_exp_requires_output_matching_modulus() {
805        let mut output = [0u8; 1];
806        let mut empty_output = [];
807
808        assert_eq!(
809            big_mod_exp(&[1], &[1], &[1, 2], &mut output),
810            Err(ProgramError::InvalidArgument)
811        );
812        assert_eq!(
813            big_mod_exp(&[1], &[1], &[], &mut empty_output),
814            Err(ProgramError::InvalidArgument)
815        );
816    }
817}