Skip to content

Commit 893cf83

Browse files
committed
native rust implementation also added for comparison out of interest
1 parent 08bb5e6 commit 893cf83

File tree

14 files changed

+2101
-17
lines changed

14 files changed

+2101
-17
lines changed

svm/Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "executor-quoter-native"
3+
version = "0.1.0"
4+
description = "Executor Quoter Native - solana_program version for CU comparison"
5+
edition = "2021"
6+
7+
[lib]
8+
crate-type = ["cdylib", "lib"]
9+
name = "executor_quoter_native"
10+
11+
[features]
12+
default = []
13+
no-entrypoint = []
14+
custom-heap = []
15+
custom-panic = []
16+
17+
[dependencies]
18+
solana-program = "1.18"
19+
bytemuck = { version = "1.14", features = ["derive"] }
20+
21+
[dev-dependencies]
22+
solana-program-test = "1.18"
23+
solana-sdk = "1.18"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use solana_program::program_error::ProgramError;
2+
3+
/// Custom error codes for ExecutorQuoter program.
4+
/// Error codes start at 0x1000 to avoid collision with built-in errors.
5+
#[repr(u32)]
6+
pub enum ExecutorQuoterError {
7+
/// Caller is not the authorized updater
8+
InvalidUpdater = 0x1000,
9+
/// Destination chain is not enabled
10+
ChainDisabled = 0x1001,
11+
/// Unsupported relay instruction type
12+
UnsupportedInstruction = 0x1002,
13+
/// Only one drop-off instruction is allowed
14+
MoreThanOneDropOff = 0x1003,
15+
/// Arithmetic overflow in quote calculation
16+
MathOverflow = 0x1004,
17+
/// Invalid relay instruction data
18+
InvalidRelayInstructions = 0x1005,
19+
/// Invalid PDA derivation
20+
InvalidPda = 0x1006,
21+
/// Account already initialized
22+
AlreadyInitialized = 0x1007,
23+
/// Account not initialized
24+
NotInitialized = 0x1008,
25+
/// Invalid account owner
26+
InvalidOwner = 0x1009,
27+
/// Invalid instruction data
28+
InvalidInstructionData = 0x100A,
29+
/// Invalid account discriminator
30+
InvalidDiscriminator = 0x100B,
31+
}
32+
33+
impl From<ExecutorQuoterError> for ProgramError {
34+
fn from(e: ExecutorQuoterError) -> Self {
35+
ProgramError::Custom(e as u32)
36+
}
37+
}
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
use solana_program::{
2+
account_info::AccountInfo, entrypoint::ProgramResult, program::set_return_data,
3+
program_error::ProgramError, pubkey::Pubkey,
4+
};
5+
6+
use crate::{
7+
error::ExecutorQuoterError,
8+
math,
9+
state::{load_account, ChainInfo, Config, QuoteBody},
10+
};
11+
12+
/// Relay instruction type constants (matching EVM)
13+
const IX_TYPE_GAS: u8 = 1;
14+
const IX_TYPE_DROP_OFF: u8 = 2;
15+
16+
/// Parses relay instructions to extract total gas limit and msg value.
17+
/// Instruction format:
18+
/// - Type 1 (Gas): 1 byte type + 16 bytes gas_limit + 16 bytes msg_value
19+
/// - Type 2 (DropOff): 1 byte type + 48 bytes (16 msg_value + 32 recipient)
20+
fn parse_relay_instructions(relay_instructions: &[u8]) -> Result<(u128, u128), ProgramError> {
21+
let mut offset = 0;
22+
let mut gas_limit: u128 = 0;
23+
let mut msg_value: u128 = 0;
24+
let mut has_drop_off = false;
25+
26+
while offset < relay_instructions.len() {
27+
if offset >= relay_instructions.len() {
28+
return Err(ExecutorQuoterError::InvalidRelayInstructions.into());
29+
}
30+
31+
let ix_type = relay_instructions[offset];
32+
offset += 1;
33+
34+
match ix_type {
35+
IX_TYPE_GAS => {
36+
// Gas instruction: 16 bytes gas_limit + 16 bytes msg_value
37+
if offset + 32 > relay_instructions.len() {
38+
return Err(ExecutorQuoterError::InvalidRelayInstructions.into());
39+
}
40+
41+
let mut ix_gas_bytes = [0u8; 16];
42+
ix_gas_bytes.copy_from_slice(&relay_instructions[offset..offset + 16]);
43+
let ix_gas_limit = u128::from_be_bytes(ix_gas_bytes);
44+
offset += 16;
45+
46+
let mut ix_val_bytes = [0u8; 16];
47+
ix_val_bytes.copy_from_slice(&relay_instructions[offset..offset + 16]);
48+
let ix_msg_value = u128::from_be_bytes(ix_val_bytes);
49+
offset += 16;
50+
51+
gas_limit = gas_limit
52+
.checked_add(ix_gas_limit)
53+
.ok_or(ExecutorQuoterError::MathOverflow)?;
54+
msg_value = msg_value
55+
.checked_add(ix_msg_value)
56+
.ok_or(ExecutorQuoterError::MathOverflow)?;
57+
}
58+
IX_TYPE_DROP_OFF => {
59+
if has_drop_off {
60+
return Err(ExecutorQuoterError::MoreThanOneDropOff.into());
61+
}
62+
has_drop_off = true;
63+
64+
// DropOff instruction: 16 bytes msg_value + 32 bytes recipient
65+
if offset + 48 > relay_instructions.len() {
66+
return Err(ExecutorQuoterError::InvalidRelayInstructions.into());
67+
}
68+
69+
let mut ix_val_bytes = [0u8; 16];
70+
ix_val_bytes.copy_from_slice(&relay_instructions[offset..offset + 16]);
71+
let ix_msg_value = u128::from_be_bytes(ix_val_bytes);
72+
offset += 48; // Skip msg_value (16) + recipient (32)
73+
74+
msg_value = msg_value
75+
.checked_add(ix_msg_value)
76+
.ok_or(ExecutorQuoterError::MathOverflow)?;
77+
}
78+
_ => {
79+
return Err(ExecutorQuoterError::UnsupportedInstruction.into());
80+
}
81+
}
82+
}
83+
84+
Ok((gas_limit, msg_value))
85+
}
86+
87+
/// Process RequestQuote instruction.
88+
/// Returns the required payment amount for cross-chain execution.
89+
///
90+
/// Accounts:
91+
/// 0. `[]` config - Config PDA
92+
/// 1. `[]` chain_info - ChainInfo PDA for destination chain
93+
/// 2. `[]` quote_body - QuoteBody PDA for destination chain
94+
///
95+
/// Instruction data layout:
96+
/// - dst_chain: u16 (offset 0)
97+
/// - dst_addr: [u8; 32] (offset 2)
98+
/// - refund_addr: [u8; 32] (offset 34)
99+
/// - request_bytes_len: u32 (offset 66)
100+
/// - request_bytes: [u8; request_bytes_len] (offset 70)
101+
/// - relay_instructions_len: u32 (offset 70 + request_bytes_len)
102+
/// - relay_instructions: [u8; relay_instructions_len]
103+
pub fn process_request_quote(
104+
program_id: &Pubkey,
105+
accounts: &[AccountInfo],
106+
data: &[u8],
107+
) -> ProgramResult {
108+
// Parse accounts
109+
let [config_account, chain_info_account, quote_body_account] = accounts else {
110+
return Err(ProgramError::NotEnoughAccountKeys);
111+
};
112+
113+
// Load accounts (discriminator checked inside load_account)
114+
// Config is validated but not used in requestQuote (only in requestExecutionQuote)
115+
let _config = load_account::<Config>(config_account, program_id)?;
116+
117+
let chain_info = load_account::<ChainInfo>(chain_info_account, program_id)?;
118+
if !chain_info.is_enabled() {
119+
return Err(ExecutorQuoterError::ChainDisabled.into());
120+
}
121+
122+
let quote_body = load_account::<QuoteBody>(quote_body_account, program_id)?;
123+
124+
// Parse instruction data to get relay_instructions
125+
// Skip: dst_chain (2) + dst_addr (32) + refund_addr (32) = 66 bytes
126+
if data.len() < 70 {
127+
return Err(ExecutorQuoterError::InvalidInstructionData.into());
128+
}
129+
130+
// Skip request_bytes
131+
let mut len_bytes = [0u8; 4];
132+
len_bytes.copy_from_slice(&data[66..70]);
133+
let request_bytes_len = u32::from_le_bytes(len_bytes) as usize;
134+
let relay_start = 70 + request_bytes_len;
135+
136+
if data.len() < relay_start + 4 {
137+
return Err(ExecutorQuoterError::InvalidInstructionData.into());
138+
}
139+
140+
let mut relay_len_bytes = [0u8; 4];
141+
relay_len_bytes.copy_from_slice(&data[relay_start..relay_start + 4]);
142+
let relay_instructions_len = u32::from_le_bytes(relay_len_bytes) as usize;
143+
144+
let relay_data_start = relay_start + 4;
145+
if data.len() < relay_data_start + relay_instructions_len {
146+
return Err(ExecutorQuoterError::InvalidInstructionData.into());
147+
}
148+
149+
let relay_instructions = &data[relay_data_start..relay_data_start + relay_instructions_len];
150+
151+
// Parse relay instructions
152+
let (gas_limit, msg_value) = parse_relay_instructions(relay_instructions)?;
153+
154+
// Calculate quote using U256 math
155+
let required_payment = math::estimate_quote(
156+
quote_body.base_fee,
157+
quote_body.src_price,
158+
quote_body.dst_price,
159+
quote_body.dst_gas_price,
160+
chain_info.gas_price_decimals,
161+
chain_info.native_decimals,
162+
gas_limit,
163+
msg_value,
164+
)?;
165+
166+
// Return the quote as big-endian U256 (32 bytes) via set_return_data.
167+
// Clients can read this via simulateTransaction or CPI callers via get_return_data.
168+
set_return_data(&required_payment.to_be_bytes());
169+
170+
Ok(())
171+
}
172+
173+
/// Process RequestExecutionQuote instruction.
174+
/// Returns the required payment, payee address, and quote body.
175+
///
176+
/// Accounts: Same as RequestQuote
177+
pub fn process_request_execution_quote(
178+
program_id: &Pubkey,
179+
accounts: &[AccountInfo],
180+
data: &[u8],
181+
) -> ProgramResult {
182+
// Parse accounts
183+
let [config_account, chain_info_account, quote_body_account] = accounts else {
184+
return Err(ProgramError::NotEnoughAccountKeys);
185+
};
186+
187+
// Load accounts (discriminator checked inside load_account)
188+
let config = load_account::<Config>(config_account, program_id)?;
189+
190+
let chain_info = load_account::<ChainInfo>(chain_info_account, program_id)?;
191+
if !chain_info.is_enabled() {
192+
return Err(ExecutorQuoterError::ChainDisabled.into());
193+
}
194+
195+
let quote_body = load_account::<QuoteBody>(quote_body_account, program_id)?;
196+
197+
// Parse instruction data to get relay_instructions
198+
if data.len() < 70 {
199+
return Err(ExecutorQuoterError::InvalidInstructionData.into());
200+
}
201+
202+
let mut len_bytes = [0u8; 4];
203+
len_bytes.copy_from_slice(&data[66..70]);
204+
let request_bytes_len = u32::from_le_bytes(len_bytes) as usize;
205+
let relay_start = 70 + request_bytes_len;
206+
207+
if data.len() < relay_start + 4 {
208+
return Err(ExecutorQuoterError::InvalidInstructionData.into());
209+
}
210+
211+
let mut relay_len_bytes = [0u8; 4];
212+
relay_len_bytes.copy_from_slice(&data[relay_start..relay_start + 4]);
213+
let relay_instructions_len = u32::from_le_bytes(relay_len_bytes) as usize;
214+
215+
let relay_data_start = relay_start + 4;
216+
if data.len() < relay_data_start + relay_instructions_len {
217+
return Err(ExecutorQuoterError::InvalidInstructionData.into());
218+
}
219+
220+
let relay_instructions = &data[relay_data_start..relay_data_start + relay_instructions_len];
221+
222+
// Parse relay instructions
223+
let (gas_limit, msg_value) = parse_relay_instructions(relay_instructions)?;
224+
225+
// Calculate quote using U256 math
226+
let required_payment = math::estimate_quote(
227+
quote_body.base_fee,
228+
quote_body.src_price,
229+
quote_body.dst_price,
230+
quote_body.dst_gas_price,
231+
chain_info.gas_price_decimals,
232+
chain_info.native_decimals,
233+
gas_limit,
234+
msg_value,
235+
)?;
236+
237+
// Return data layout (96 bytes, matching EVM return values):
238+
// - bytes 0-31: required_payment (U256, big-endian)
239+
// - bytes 32-63: payee_address (32 bytes)
240+
// - bytes 64-95: quote_body (32 bytes, EQ01 format)
241+
let mut return_data = [0u8; 96];
242+
return_data[0..32].copy_from_slice(&required_payment.to_be_bytes());
243+
return_data[32..64].copy_from_slice(&config.payee_address);
244+
return_data[64..96].copy_from_slice(&quote_body.to_bytes32());
245+
246+
set_return_data(&return_data);
247+
248+
Ok(())
249+
}

0 commit comments

Comments
 (0)