Skip to main content

hpsvm_fixture_fd/
lib.rs

1#![allow(missing_debug_implementations, missing_docs)]
2#![deny(rustdoc::broken_intra_doc_links)]
3
4mod error;
5
6use std::{fs, path::Path};
7#[cfg(feature = "json-codec")]
8use std::{
9    fs::File,
10    io::{BufReader, BufWriter},
11};
12
13use hpsvm_fixture::{
14    AccountCompareScope, AccountSnapshot, Compare, ExecutionSnapshot, ExecutionSnapshotFields,
15    ExecutionStatus, FixtureExpectations, FixtureHeader, FixtureInput, FixtureKind,
16    InstructionAccountMeta, InstructionFixture, ReturnDataSnapshot, RuntimeFixtureConfig,
17};
18use mollusk_svm_fuzz_fixture_firedancer as fd_codec;
19use prost::Message;
20use solana_address::Address;
21
22pub use crate::error::AdapterError;
23
24const FIREDANCER_SOURCE: &str = "firedancer";
25const FIREDANCER_TAG: &str = "external:firedancer";
26
27#[derive(Clone, Debug, PartialEq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub struct FiredancerFixture {
30    inner: fd_codec::proto::InstrFixture,
31}
32
33impl FiredancerFixture {
34    pub fn from_proto(inner: fd_codec::proto::InstrFixture) -> Self {
35        Self { inner }
36    }
37
38    pub fn as_proto(&self) -> &fd_codec::proto::InstrFixture {
39        &self.inner
40    }
41
42    pub fn into_proto(self) -> fd_codec::proto::InstrFixture {
43        self.inner
44    }
45
46    #[must_use]
47    pub fn to_model(&self) -> fd_codec::Fixture {
48        self.inner.clone().into()
49    }
50
51    pub fn load(path: impl AsRef<Path>) -> Result<Self, AdapterError> {
52        let path = path.as_ref();
53        match fixture_format_for_path(path)? {
54            FiredancerFixtureFormat::Binary => {
55                let bytes = fs::read(path)?;
56                let inner = fd_codec::proto::InstrFixture::decode(bytes.as_slice())?;
57                Ok(Self { inner })
58            }
59            #[cfg(feature = "json-codec")]
60            FiredancerFixtureFormat::Json => {
61                let reader = BufReader::new(File::open(path)?);
62                let inner = serde_json::from_reader(reader)?;
63                Ok(Self { inner })
64            }
65        }
66    }
67
68    pub fn save(&self, path: impl AsRef<Path>) -> Result<(), AdapterError> {
69        let path = path.as_ref();
70        match fixture_format_for_path(path)? {
71            FiredancerFixtureFormat::Binary => fs::write(path, self.inner.encode_to_vec())?,
72            #[cfg(feature = "json-codec")]
73            FiredancerFixtureFormat::Json => {
74                let writer = BufWriter::new(File::create(path)?);
75                serde_json::to_writer_pretty(writer, &self.inner)?;
76            }
77        }
78        Ok(())
79    }
80}
81
82impl From<fd_codec::Fixture> for FiredancerFixture {
83    fn from(value: fd_codec::Fixture) -> Self {
84        Self { inner: value.into() }
85    }
86}
87
88impl From<fd_codec::proto::InstrFixture> for FiredancerFixture {
89    fn from(value: fd_codec::proto::InstrFixture) -> Self {
90        Self::from_proto(value)
91    }
92}
93
94impl From<FiredancerFixture> for fd_codec::proto::InstrFixture {
95    fn from(value: FiredancerFixture) -> Self {
96        value.inner
97    }
98}
99
100impl TryFrom<FiredancerFixture> for hpsvm_fixture::Fixture {
101    type Error = AdapterError;
102
103    fn try_from(value: FiredancerFixture) -> Result<Self, Self::Error> {
104        let fd_codec::proto::InstrFixture { metadata, input, output } = value.inner;
105        let input = input.ok_or(AdapterError::MissingField { field: "input" })?;
106        let output = output.ok_or(AdapterError::MissingField { field: "output" })?;
107
108        let pre_accounts = input
109            .accounts
110            .into_iter()
111            .map(|account| account_snapshot_from_proto(account, "input.accounts"))
112            .collect::<Result<Vec<_>, _>>()?;
113
114        let instruction_accounts = input
115            .instr_accounts
116            .into_iter()
117            .map(|account| instruction_account_from_proto(&pre_accounts, account))
118            .collect::<Result<Vec<_>, _>>()?;
119
120        let post_accounts = output
121            .modified_accounts
122            .into_iter()
123            .map(|account| account_snapshot_from_proto(account, "output.modified_accounts"))
124            .collect::<Result<Vec<_>, _>>()?;
125
126        let program_id = address_from_bytes(&input.program_id, "input.program_id")?;
127        let compute_units_consumed = input.cu_avail.checked_sub(output.cu_avail).ok_or(
128            AdapterError::InconsistentComputeUnits {
129                before: input.cu_avail,
130                after: output.cu_avail,
131            },
132        )?;
133        let baseline = ExecutionSnapshot::from_fields(ExecutionSnapshotFields {
134            status: status_from_output(output.result, output.custom_err)?,
135            included: true,
136            compute_units_consumed,
137            fee: 0,
138            logs: Vec::new(),
139            return_data: return_data_from_output(program_id, output.return_data),
140            inner_instructions: Vec::new(),
141            post_accounts: post_accounts.clone(),
142        });
143
144        let header = FixtureHeader::new(
145            metadata
146                .as_ref()
147                .map(|value| value.fn_entrypoint.as_str())
148                .filter(|value| !value.is_empty())
149                .map_or_else(|| format!("firedancer-{program_id}"), str::to_owned),
150            FixtureKind::Instruction,
151        )
152        .source(FIREDANCER_SOURCE)
153        .tag(FIREDANCER_TAG);
154
155        Ok(Self::new(
156            header,
157            FixtureInput::Instruction(InstructionFixture::new(
158                RuntimeFixtureConfig::new(
159                    input.slot_context.map_or(0, |slot| slot.slot),
160                    None,
161                    false,
162                    false,
163                )
164                .with_compute_unit_limit(input.cu_avail),
165                Vec::new(),
166                pre_accounts,
167                program_id,
168                instruction_accounts,
169                input.data,
170            )),
171            FixtureExpectations::new(baseline, compares_for_post_accounts(&post_accounts)),
172        ))
173    }
174}
175
176impl TryFrom<hpsvm_fixture::Fixture> for FiredancerFixture {
177    type Error = AdapterError;
178
179    fn try_from(value: hpsvm_fixture::Fixture) -> Result<Self, Self::Error> {
180        match value.input {
181            FixtureInput::Instruction(instruction) => {
182                let input_cu_avail = instruction
183                    .runtime
184                    .compute_unit_limit
185                    .ok_or(AdapterError::MissingComputeUnitBudget)?;
186                let output_cu_avail = input_cu_avail
187                    .checked_sub(value.expectations.baseline.compute_units_consumed)
188                    .ok_or(AdapterError::ComputeUnitsExceedBudget {
189                        budget: input_cu_avail,
190                        consumed: value.expectations.baseline.compute_units_consumed,
191                    })?;
192                let input = fd_codec::proto::InstrContext {
193                    program_id: address_to_bytes(instruction.program_id),
194                    accounts: instruction
195                        .pre_accounts
196                        .iter()
197                        .map(account_snapshot_to_proto)
198                        .collect(),
199                    instr_accounts: instruction_accounts_to_proto(
200                        &instruction.pre_accounts,
201                        &instruction.accounts,
202                    )?,
203                    data: instruction.data,
204                    cu_avail: input_cu_avail,
205                    slot_context: Some(fd_codec::proto::SlotContext {
206                        slot: instruction.runtime.slot,
207                    }),
208                    epoch_context: None,
209                };
210                let output = snapshot_to_proto_effects(
211                    &value.expectations.baseline,
212                    instruction.program_id,
213                    output_cu_avail,
214                )?;
215
216                Ok(Self::from_proto(fd_codec::proto::InstrFixture {
217                    metadata: Some(fd_codec::proto::FixtureMetadata {
218                        fn_entrypoint: value.header.name,
219                    }),
220                    input: Some(input),
221                    output: Some(output),
222                }))
223            }
224            FixtureInput::Transaction(_) => Err(AdapterError::UnsupportedFixtureKind {
225                kind: "transaction",
226                expected: "instruction",
227            }),
228            _ => Err(AdapterError::UnsupportedFixtureKind {
229                kind: "unknown",
230                expected: "instruction",
231            }),
232        }
233    }
234}
235
236#[derive(Clone, Copy, Debug, PartialEq, Eq)]
237enum FiredancerFixtureFormat {
238    Binary,
239    #[cfg(feature = "json-codec")]
240    Json,
241}
242
243fn fixture_format_for_path(path: &Path) -> Result<FiredancerFixtureFormat, AdapterError> {
244    match path.extension().and_then(|value| value.to_str()) {
245        Some("fix") => Ok(FiredancerFixtureFormat::Binary),
246        #[cfg(feature = "json-codec")]
247        Some("json") => Ok(FiredancerFixtureFormat::Json),
248        _ => Err(AdapterError::UnsupportedFormat { path: path.display().to_string() }),
249    }
250}
251
252fn address_from_bytes(bytes: &[u8], field: &'static str) -> Result<Address, AdapterError> {
253    let array: [u8; 32] = bytes
254        .try_into()
255        .map_err(|_| AdapterError::InvalidAddressLength { field, actual: bytes.len() })?;
256    Ok(Address::new_from_array(array))
257}
258
259fn account_snapshot_from_proto(
260    account: fd_codec::proto::AcctState,
261    field: &'static str,
262) -> Result<AccountSnapshot, AdapterError> {
263    if account.seed_addr.is_some() {
264        return Err(AdapterError::UnsupportedSeedAddress { field });
265    }
266
267    Ok(AccountSnapshot::new(
268        address_from_bytes(&account.address, "account.address")?,
269        account.lamports,
270        address_from_bytes(&account.owner, "account.owner")?,
271        account.executable,
272        account.rent_epoch,
273        account.data,
274    ))
275}
276
277fn account_snapshot_to_proto(account: &AccountSnapshot) -> fd_codec::proto::AcctState {
278    fd_codec::proto::AcctState {
279        address: address_to_bytes(account.address),
280        lamports: account.lamports,
281        data: account.data.clone(),
282        executable: account.executable,
283        rent_epoch: account.rent_epoch,
284        owner: address_to_bytes(account.owner),
285        seed_addr: None,
286    }
287}
288
289fn address_to_bytes(address: Address) -> Vec<u8> {
290    address.to_bytes().to_vec()
291}
292
293fn instruction_account_from_proto(
294    accounts: &[AccountSnapshot],
295    account: fd_codec::proto::InstrAcct,
296) -> Result<InstructionAccountMeta, AdapterError> {
297    let index = usize::try_from(account.index).map_err(|_| {
298        AdapterError::InvalidInstructionAccountIndex {
299            index: usize::MAX,
300            accounts_len: accounts.len(),
301        }
302    })?;
303    let address = accounts
304        .get(index)
305        .ok_or(AdapterError::InvalidInstructionAccountIndex {
306            index,
307            accounts_len: accounts.len(),
308        })?
309        .address;
310    Ok(InstructionAccountMeta::new(address, account.is_signer, account.is_writable))
311}
312
313fn instruction_accounts_to_proto(
314    accounts: &[AccountSnapshot],
315    instruction_accounts: &[InstructionAccountMeta],
316) -> Result<Vec<fd_codec::proto::InstrAcct>, AdapterError> {
317    instruction_accounts
318        .iter()
319        .map(|account| {
320            let index = accounts
321                .iter()
322                .position(|candidate| candidate.address == account.pubkey)
323                .ok_or_else(|| AdapterError::MissingInstructionAccount {
324                address: account.pubkey.to_string(),
325            })?;
326            Ok(fd_codec::proto::InstrAcct {
327                index: u32::try_from(index).map_err(|_| {
328                    AdapterError::InvalidInstructionAccountIndex {
329                        index,
330                        accounts_len: accounts.len(),
331                    }
332                })?,
333                is_writable: account.is_writable,
334                is_signer: account.is_signer,
335            })
336        })
337        .collect()
338}
339
340fn snapshot_to_proto_effects(
341    snapshot: &ExecutionSnapshot,
342    instruction_program_id: Address,
343    cu_avail: u64,
344) -> Result<fd_codec::proto::InstrEffects, AdapterError> {
345    let (result, custom_err) = status_to_output(&snapshot.status)?;
346    let return_data = snapshot
347        .return_data
348        .as_ref()
349        .map(|return_data| {
350            if return_data.program_id == instruction_program_id {
351                Ok(return_data.data.clone())
352            } else {
353                Err(AdapterError::UnsupportedReturnDataProgram {
354                    program_id: return_data.program_id.to_string(),
355                    instruction_program_id: instruction_program_id.to_string(),
356                })
357            }
358        })
359        .transpose()?
360        .unwrap_or_default();
361
362    Ok(fd_codec::proto::InstrEffects {
363        result,
364        custom_err,
365        modified_accounts: snapshot.post_accounts.iter().map(account_snapshot_to_proto).collect(),
366        cu_avail,
367        return_data,
368    })
369}
370
371fn status_to_output(status: &ExecutionStatus) -> Result<(i32, u32), AdapterError> {
372    match status {
373        ExecutionStatus::Success => Ok((0, 0)),
374        ExecutionStatus::Failure { kind, .. } => {
375            if let Some(value) = parse_firedancer_result_with_custom_error(kind) {
376                Ok(value)
377            } else if let Some(value) = parse_firedancer_custom_error(kind) {
378                Ok((1, value))
379            } else if let Some(value) = parse_firedancer_program_result(kind) {
380                Ok((value, 0))
381            } else {
382                Err(AdapterError::UnsupportedExecutionStatus { kind: kind.clone() })
383            }
384        }
385        status => Err(AdapterError::UnsupportedExecutionStatus { kind: format!("{status:?}") }),
386    }
387}
388
389fn parse_firedancer_result_with_custom_error(kind: &str) -> Option<(i32, u32)> {
390    let inner =
391        kind.strip_prefix("FiredancerProgramResult(").and_then(|value| value.strip_suffix(')'))?;
392    let (result, custom_err) = inner.split_once(",CustomError(")?;
393    Some((result.parse().ok()?, custom_err.strip_suffix(')')?.parse().ok()?))
394}
395
396fn parse_firedancer_custom_error(kind: &str) -> Option<u32> {
397    kind.strip_prefix("FiredancerCustomError(")
398        .and_then(|value| value.strip_suffix(')'))
399        .and_then(|value| value.parse::<u32>().ok())
400}
401
402fn parse_firedancer_program_result(kind: &str) -> Option<i32> {
403    kind.strip_prefix("FiredancerProgramResult(")
404        .and_then(|value| value.strip_suffix(')'))
405        .and_then(|value| value.parse::<i32>().ok())
406}
407
408fn status_from_output(result: i32, custom_err: u32) -> Result<ExecutionStatus, AdapterError> {
409    match (result, custom_err) {
410        (0, 0) => Ok(ExecutionStatus::Success),
411        (0, custom_err) => Err(AdapterError::InconsistentExecutionStatus { result, custom_err }),
412        (result, 0) => Ok(ExecutionStatus::Failure {
413            kind: format!("FiredancerProgramResult({result})"),
414            message: format!("firedancer program returned status {result}"),
415        }),
416        (result, custom_err) => Ok(ExecutionStatus::Failure {
417            kind: format!("FiredancerProgramResult({result},CustomError({custom_err}))"),
418            message: format!(
419                "firedancer program returned status {result} with custom error {custom_err}"
420            ),
421        }),
422    }
423}
424
425fn return_data_from_output(
426    program_id: Address,
427    return_data: Vec<u8>,
428) -> Option<ReturnDataSnapshot> {
429    if return_data.is_empty() {
430        None
431    } else {
432        Some(ReturnDataSnapshot::new(program_id, return_data))
433    }
434}
435
436fn compares_for_post_accounts(post_accounts: &[AccountSnapshot]) -> Vec<Compare> {
437    let mut compares =
438        vec![Compare::Status, Compare::Included, Compare::ComputeUnits, Compare::ReturnData];
439
440    if !post_accounts.is_empty() {
441        let mut addresses = Vec::with_capacity(post_accounts.len());
442        for account in post_accounts {
443            if !addresses.contains(&account.address) {
444                addresses.push(account.address);
445            }
446        }
447        compares.push(Compare::Accounts(AccountCompareScope::Only(addresses)));
448    }
449
450    compares
451}