Skip to content

Commit 26b37d8

Browse files
committed
final implementation before testing
1 parent b0d27e9 commit 26b37d8

File tree

20 files changed

+464
-261
lines changed

20 files changed

+464
-261
lines changed

svm/modules/executor-requests/Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

svm/modules/executor-requests/src/lib.rs

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,105 @@ impl RelayInstructionsBuilder {
145145
}
146146
}
147147

148+
// ============================================================================
149+
// Relay Instruction Parsing
150+
// ============================================================================
151+
152+
/// Relay instruction parsing errors.
153+
///
154+
/// Discriminants are ordered to align with executor-quoter error codes (base 0x1002):
155+
/// - 0 -> UnsupportedInstruction (0x1002)
156+
/// - 1 -> MoreThanOneDropOff (0x1003)
157+
/// - 2 -> MathOverflow (0x1004)
158+
/// - 3 -> InvalidRelayInstructions (0x1005)
159+
#[repr(u8)]
160+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161+
pub enum RelayParseError {
162+
/// Unknown relay instruction type
163+
UnsupportedType = 0,
164+
/// More than one drop-off instruction found
165+
MultipleDropoff = 1,
166+
/// Arithmetic overflow when accumulating gas_limit or msg_value
167+
Overflow = 2,
168+
/// Instruction data truncated / not enough bytes
169+
Truncated = 3,
170+
}
171+
172+
/// Parses relay instructions to extract total gas limit and msg value.
173+
///
174+
/// Returns `(gas_limit, msg_value)` on success, or `RelayParseError` on failure.
175+
/// Multiple gas instructions are summed. Only one dropoff is allowed.
176+
///
177+
/// Instruction format:
178+
/// - Type 1 (Gas): 1 byte type + 16 bytes gas_limit + 16 bytes msg_value = 33 bytes
179+
/// - Type 2 (DropOff): 1 byte type + 16 bytes msg_value + 32 bytes recipient = 49 bytes
180+
pub fn parse_relay_instructions(data: &[u8]) -> Result<(u128, u128), RelayParseError> {
181+
let mut offset = 0;
182+
let mut gas_limit: u128 = 0;
183+
let mut msg_value: u128 = 0;
184+
let mut has_drop_off = false;
185+
186+
while offset < data.len() {
187+
let ix_type = data[offset];
188+
offset += 1;
189+
190+
match ix_type {
191+
RELAY_IX_GAS => {
192+
// Gas instruction: 16 bytes gas_limit + 16 bytes msg_value
193+
if offset + 32 > data.len() {
194+
return Err(RelayParseError::Truncated);
195+
}
196+
197+
let mut ix_gas_bytes = [0u8; 16];
198+
ix_gas_bytes.copy_from_slice(&data[offset..offset + 16]);
199+
let ix_gas_limit = u128::from_be_bytes(ix_gas_bytes);
200+
offset += 16;
201+
202+
let mut ix_val_bytes = [0u8; 16];
203+
ix_val_bytes.copy_from_slice(&data[offset..offset + 16]);
204+
let ix_msg_value = u128::from_be_bytes(ix_val_bytes);
205+
offset += 16;
206+
207+
gas_limit = gas_limit
208+
.checked_add(ix_gas_limit)
209+
.ok_or(RelayParseError::Overflow)?;
210+
msg_value = msg_value
211+
.checked_add(ix_msg_value)
212+
.ok_or(RelayParseError::Overflow)?;
213+
}
214+
RELAY_IX_GAS_DROP_OFF => {
215+
if has_drop_off {
216+
return Err(RelayParseError::MultipleDropoff);
217+
}
218+
has_drop_off = true;
219+
220+
// DropOff instruction: 16 bytes msg_value + 32 bytes recipient
221+
if offset + 48 > data.len() {
222+
return Err(RelayParseError::Truncated);
223+
}
224+
225+
let mut ix_val_bytes = [0u8; 16];
226+
ix_val_bytes.copy_from_slice(&data[offset..offset + 16]);
227+
let ix_msg_value = u128::from_be_bytes(ix_val_bytes);
228+
offset += 48; // Skip msg_value (16) + recipient (32)
229+
230+
msg_value = msg_value
231+
.checked_add(ix_msg_value)
232+
.ok_or(RelayParseError::Overflow)?;
233+
}
234+
_ => {
235+
return Err(RelayParseError::UnsupportedType);
236+
}
237+
}
238+
}
239+
240+
Ok((gas_limit, msg_value))
241+
}
242+
148243
#[cfg(test)]
149244
mod tests {
150245
use super::*;
246+
use alloc::vec;
151247

152248
#[test]
153249
fn test_vaa_v1() {
@@ -272,4 +368,141 @@ mod tests {
272368
let result = RelayInstructionsBuilder::new().build();
273369
assert_eq!(result.len(), 0);
274370
}
371+
372+
// ========================================================================
373+
// parse_relay_instructions tests
374+
// ========================================================================
375+
376+
#[test]
377+
fn test_parse_relay_instructions_empty() {
378+
let result = parse_relay_instructions(&[]);
379+
assert_eq!(result, Ok((0, 0)));
380+
}
381+
382+
#[test]
383+
fn test_parse_relay_instructions_gas() {
384+
let data = make_relay_instruction_gas(250_000, 1_000_000);
385+
let result = parse_relay_instructions(&data);
386+
assert_eq!(result, Ok((250_000, 1_000_000)));
387+
}
388+
389+
#[test]
390+
fn test_parse_relay_instructions_dropoff() {
391+
let recipient = [0xAB; 32];
392+
let data = make_relay_instruction_gas_drop_off(500_000, &recipient);
393+
let result = parse_relay_instructions(&data);
394+
// DropOff contributes to msg_value, not gas_limit
395+
assert_eq!(result, Ok((0, 500_000)));
396+
}
397+
398+
#[test]
399+
fn test_parse_relay_instructions_gas_and_dropoff() {
400+
let recipient = [0xCD; 32];
401+
let data = RelayInstructionsBuilder::new()
402+
.with_gas(100_000, 200_000)
403+
.with_gas_drop_off(300_000, &recipient)
404+
.build();
405+
let result = parse_relay_instructions(&data);
406+
// gas_limit = 100_000, msg_value = 200_000 + 300_000 = 500_000
407+
assert_eq!(result, Ok((100_000, 500_000)));
408+
}
409+
410+
#[test]
411+
fn test_parse_relay_instructions_multiple_gas() {
412+
let mut data = make_relay_instruction_gas(100_000, 50_000);
413+
data.extend(make_relay_instruction_gas(200_000, 75_000));
414+
data.extend(make_relay_instruction_gas(50_000, 25_000));
415+
let result = parse_relay_instructions(&data);
416+
// gas_limit = 100k + 200k + 50k = 350k
417+
// msg_value = 50k + 75k + 25k = 150k
418+
assert_eq!(result, Ok((350_000, 150_000)));
419+
}
420+
421+
#[test]
422+
fn test_parse_relay_instructions_invalid_type() {
423+
let data = [0xFF, 0x00, 0x00]; // Invalid type 0xFF
424+
let result = parse_relay_instructions(&data);
425+
assert_eq!(result, Err(RelayParseError::UnsupportedType));
426+
}
427+
428+
#[test]
429+
fn test_parse_relay_instructions_truncated_gas() {
430+
// Gas instruction needs 33 bytes (1 type + 16 gas_limit + 16 msg_value)
431+
// Provide only 10 bytes after type
432+
let mut data = vec![RELAY_IX_GAS];
433+
data.extend_from_slice(&[0u8; 10]);
434+
let result = parse_relay_instructions(&data);
435+
assert_eq!(result, Err(RelayParseError::Truncated));
436+
}
437+
438+
#[test]
439+
fn test_parse_relay_instructions_truncated_dropoff() {
440+
// DropOff instruction needs 49 bytes (1 type + 16 msg_value + 32 recipient)
441+
// Provide only 20 bytes after type
442+
let mut data = vec![RELAY_IX_GAS_DROP_OFF];
443+
data.extend_from_slice(&[0u8; 20]);
444+
let result = parse_relay_instructions(&data);
445+
assert_eq!(result, Err(RelayParseError::Truncated));
446+
}
447+
448+
#[test]
449+
fn test_parse_relay_instructions_multiple_dropoff() {
450+
let recipient = [0xAB; 32];
451+
let mut data = make_relay_instruction_gas_drop_off(100_000, &recipient);
452+
data.extend(make_relay_instruction_gas_drop_off(200_000, &recipient));
453+
let result = parse_relay_instructions(&data);
454+
assert_eq!(result, Err(RelayParseError::MultipleDropoff));
455+
}
456+
457+
#[test]
458+
fn test_parse_relay_instructions_overflow_gas_limit() {
459+
let mut data = make_relay_instruction_gas(u128::MAX, 0);
460+
data.extend(make_relay_instruction_gas(1, 0)); // This should overflow
461+
let result = parse_relay_instructions(&data);
462+
assert_eq!(result, Err(RelayParseError::Overflow));
463+
}
464+
465+
#[test]
466+
fn test_parse_relay_instructions_overflow_msg_value() {
467+
let mut data = make_relay_instruction_gas(0, u128::MAX);
468+
data.extend(make_relay_instruction_gas(0, 1)); // This should overflow
469+
let result = parse_relay_instructions(&data);
470+
assert_eq!(result, Err(RelayParseError::Overflow));
471+
}
472+
473+
// Roundtrip tests
474+
475+
#[test]
476+
fn test_roundtrip_gas() {
477+
let gas_limit = 1_000_000u128;
478+
let msg_value = 2_000_000_000_000_000_000u128; // 2 ETH in wei
479+
let data = make_relay_instruction_gas(gas_limit, msg_value);
480+
let result = parse_relay_instructions(&data);
481+
assert_eq!(result, Ok((gas_limit, msg_value)));
482+
}
483+
484+
#[test]
485+
fn test_roundtrip_dropoff() {
486+
let drop_off = 500_000_000_000_000_000u128; // 0.5 ETH in wei
487+
let recipient = [0x42; 32];
488+
let data = make_relay_instruction_gas_drop_off(drop_off, &recipient);
489+
let result = parse_relay_instructions(&data);
490+
assert_eq!(result, Ok((0, drop_off)));
491+
}
492+
493+
#[test]
494+
fn test_roundtrip_builder() {
495+
let gas_limit = 300_000u128;
496+
let gas_msg_value = 100_000_000_000_000_000u128; // 0.1 ETH
497+
let drop_off = 250_000_000_000_000_000u128; // 0.25 ETH
498+
let recipient = [0x99; 32];
499+
500+
let data = RelayInstructionsBuilder::new()
501+
.with_gas(gas_limit, gas_msg_value)
502+
.with_gas_drop_off(drop_off, &recipient)
503+
.build();
504+
505+
let result = parse_relay_instructions(&data);
506+
assert_eq!(result, Ok((gas_limit, gas_msg_value + drop_off)));
507+
}
275508
}

svm/pinocchio/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

svm/pinocchio/README.md

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
11
# Pinocchio Programs
22

3-
This directory contains Solana programs built with the Pinocchio framework for the executor quoter system.
3+
Solana programs for the executor quoter system.
4+
5+
## Overview
6+
7+
- **executor-quoter-router** - Defines the quoter interface specification and routes CPI calls to registered quoter implementations. See [programs/executor-quoter-router/README.md](programs/executor-quoter-router/README.md).
8+
- **executor-quoter** - Example quoter implementation. Integrators can use this as a reference or build their own. See [programs/executor-quoter/README.md](programs/executor-quoter/README.md).
9+
10+
These programs use the [Pinocchio](https://github.com/febo/pinocchio) framework, but quoter implementations are framework-agnostic. Any program adhering to the CPI interface defined by the router will work.
411

512
## Directory Structure
613

7-
- `programs/executor-quoter/` - Quoter program for price quotes
8-
- `programs/executor-quoter-router/` - Router program for quoter registration and execution routing
14+
- `programs/executor-quoter/` - Example quoter implementation
15+
- `programs/executor-quoter-router/` - Router program defining the quoter spec
916
- `tests/executor-quoter-tests/` - Integration tests and benchmarks for executor-quoter
1017
- `tests/executor-quoter-router-tests/` - Integration tests and benchmarks for executor-quoter-router
1118

1219
## Prerequisites
1320

1421
- Solana CLI v1.18.17 or later
15-
- A keypair file for the quoter updater address
1622

17-
Generate a test keypair if you don't have one:
23+
### Testing Prerequisites
24+
25+
Generate test keypairs before building or running tests:
1826

1927
```bash
2028
mkdir -p ../test-keys
2129
solana-keygen new --no-bip39-passphrase -o ../test-keys/quoter-updater.json
30+
solana-keygen new --no-bip39-passphrase -o ../test-keys/quoter-payee.json
2231
```
2332

2433
## Building
@@ -72,11 +81,13 @@ export SBF_OUT_DIR=$(pwd)/target/deploy
7281
# Run unit tests (pure Rust math module)
7382
cargo test -p executor-quoter
7483

75-
# Run integration tests (uses mollusk-svm to simulate program execution)
76-
cargo test -p executor-quoter-tests -p executor-quoter-router-tests
84+
# Run integration tests (uses solana-program-test to simulate program execution)
85+
cargo test -p executor-quoter-tests -p executor-quoter-router-tests -- --test-threads=1
7786
```
7887

79-
Note: These tests use native `cargo test`, not `cargo test-sbf`. The unit tests are pure Rust without SBF dependencies. The integration tests use mollusk-svm which loads the pre-built `.so` files and simulates program execution natively.
88+
Note: These tests use native `cargo test`, not `cargo test-sbf`. The unit tests are pure Rust without SBF dependencies. The integration tests use solana-program-test which loads the pre-built `.so` files and simulates program execution natively.
89+
90+
The `--test-threads=1` flag is required because `solana-program-test` can exhibit race conditions when multiple tests load BPF programs in parallel. Running tests sequentially avoids these issues.
8091

8192
## Running Benchmarks
8293

@@ -92,6 +103,6 @@ cargo bench -p executor-quoter-router-tests
92103

93104
## Notes
94105

95-
- The test crates use `solana-program-test` and [mollusk-svm](https://github.com/buffalojoec/mollusk) to load and execute the compiled `.so` files in a simulated SVM environment.
106+
- The test crates use `solana-program-test` to load and execute the compiled `.so` files in a simulated SVM environment. Benchmarks use [mollusk-svm](https://github.com/buffalojoec/mollusk) for compute unit measurements.
96107
- Tests will fail if the `.so` files are not built first.
97108
- The `QUOTER_UPDATER_PUBKEY` is baked into the program at compile time and cannot be changed without rebuilding.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Executor Quoter Router
2+
3+
Router program that dispatches quote requests and execution requests to registered quoter implementations.
4+
5+
## Overview
6+
7+
The router manages quoter registrations and routes CPI calls to the appropriate quoter program. It defines the interface that quoter implementations must adhere to.
8+
9+
## Instructions
10+
11+
**UpdateQuoterContract (discriminator: 0)**
12+
- Registers or updates a quoter's implementation mapping
13+
- Accounts: `[payer, sender, config, quoter_registration, system_program]`
14+
15+
**QuoteExecution (discriminator: 1)**
16+
- Gets a quote from a registered quoter via CPI
17+
- Accounts: `[quoter_registration, quoter_program, config, chain_info, quote_body]`
18+
- CPI to quoter's `RequestQuote` instruction (discriminator: `[2, 0, 0, 0, 0, 0, 0, 0]`)
19+
20+
**RequestExecution (discriminator: 2)**
21+
- Executes cross-chain request through the router
22+
- Accounts: `[payer, config, quoter_registration, quoter_program, executor_program, payee, refund_addr, system_program, quoter_config, chain_info, quote_body, event_cpi]`
23+
- CPI to quoter's `RequestExecutionQuote` instruction (discriminator: `[3, 0, 0, 0, 0, 0, 0, 0]`)
24+
25+
## Quoter Interface Requirements
26+
27+
Quoter implementations must support the following CPI interface:
28+
29+
### RequestQuote
30+
- Discriminator: 8 bytes (`[2, 0, 0, 0, 0, 0, 0, 0]`)
31+
- Accounts: `[config, chain_info, quote_body]`
32+
- Returns: `u64` (big endian) payment amount via `set_return_data`
33+
34+
### RequestExecutionQuote
35+
- Discriminator: 8 bytes (`[3, 0, 0, 0, 0, 0, 0, 0]`)
36+
- Accounts: `[config, chain_info, quote_body, event_cpi]`
37+
- Returns: 72 bytes via `set_return_data`:
38+
- bytes 0-7: `u64` required payment (big-endian)
39+
- bytes 8-39: 32-byte payee address
40+
- bytes 40-71: 32-byte quote body (EQ01 format)
41+
42+
See `executor-quoter` for a reference implementation.

svm/pinocchio/programs/executor-quoter-router/src/instructions/serialization.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ pub fn make_signed_quote_eq02(
5353
/// - bytes 130-161: signature_s (32 bytes)
5454
/// - byte 162: signature_v (1 byte)
5555
#[derive(Debug, Clone, Copy)]
56-
#[allow(dead_code)]
5756
pub struct GovernanceMessage {
5857
pub chain_id: u16,
5958
pub quoter_address: [u8; 20],

0 commit comments

Comments
 (0)