Skip to content

Commit ef28038

Browse files
committed
feat: forked transaction-view for increased limits
1 parent 8af975f commit ef28038

11 files changed

Lines changed: 128 additions & 238 deletions

File tree

‎Cargo.toml‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace]
2-
members = ["solana/account"]
2+
members = ["solana/account", "solana/transaction-view"]
33
resolver = "3"
44

55
[workspace.package]
@@ -21,6 +21,7 @@ arc-swap = "1.9.1"
2121
bincode = "1.3.3"
2222
bitflags = "2.11.1"
2323
blake3 = "1.8.5"
24+
criterion = "0.8.2"
2425
qualifier_attr = "0.2.2"
2526
rand = "0.8.5"
2627
rustix = { version = "1.1.4" }
@@ -44,11 +45,15 @@ solana-cpi = "3.1.0"
4445
solana-hash = "4.3.0"
4546
solana-instruction-error = "2.3.0"
4647
solana-program-error = "3.0.1"
48+
solana-packet = "4.1.0"
4749
solana-pubkey = "4.2.0"
4850
solana-sdk-ids = "3.1.0"
51+
solana-short-vec = "3.2.1"
4952
solana-sysvar = "4.0.0"
5053
solana-transaction-error = "3.2.0"
5154

55+
[patch.crates-io]
56+
agave-transaction-view = { path = "solana/transaction-view" }
5257

5358
[workspace.lints.rust]
5459
missing_docs = "deny"

‎solana/transaction-view/Cargo.toml‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
[package]
2-
name = "agave-transaction-view"
2+
authors = { workspace = true }
33
description = "Zero-copy parser and sanitizer for serialized Solana transactions"
44
documentation = "https://docs.rs/agave-transaction-view"
5-
version = { workspace = true }
6-
authors = { workspace = true }
7-
repository = { workspace = true }
5+
edition = "2024"
86
homepage = { workspace = true }
97
license = { workspace = true }
10-
edition = "2024"
8+
name = "agave-transaction-view"
9+
readme = "README.md"
10+
repository = { workspace = true }
11+
version = "4.1.1"
1112

1213
[features]
1314
agave-unstable-api = []
@@ -41,9 +42,9 @@ solana-transaction = { workspace = true, features = ["serde", "wincode"] }
4142
wincode = { workspace = true }
4243

4344
[[bench]]
44-
name = "bytes"
4545
harness = false
46+
name = "bytes"
4647

4748
[[bench]]
48-
name = "transaction_view"
4949
harness = false
50+
name = "transaction_view"

‎solana/transaction-view/README.md‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# `agave-transaction-view`
2+
3+
This Agave fork parses and sanitizes serialized transactions without fully
4+
deserializing them. Workspace `[patch.crates-io]` entries force the dependency
5+
graph to use this copy.
6+
7+
The view owns or borrows the original transaction bytes through
8+
`TransactionData` and caches only framing metadata. Accessors and iterators then
9+
read signatures, account keys, instructions, configuration, and address-table
10+
metadata directly from the validated byte layout.
11+
12+
## Supported formats
13+
14+
- Legacy and v0 use the standard Solana wire layouts.
15+
- V1 uses the Agave V1 layout with instruction headers, contiguous payloads,
16+
transaction configuration, and trailing signatures.
17+
- `Magicblock` is the Engine-private version `127`. It uses the V1 layout and a
18+
distinct version prefix; it is not a client-facing Solana transaction version.
19+
20+
For V1 and Magicblock transactions, `message_data()` covers the version byte
21+
through the instruction payloads and excludes the trailing signatures. Engine
22+
construction must sign exactly that range after writing the final version
23+
prefix.
24+
25+
## Limits and parsing invariants
26+
27+
Legacy, v0, and V1 transactions may be at most `u16::MAX` bytes, inclusive.
28+
Magicblock transactions may be at most 16 MiB and may use an instruction trace
29+
length of 255. Other structural limits, including the standard signature and
30+
account-index limits, remain enforced by sanitization.
31+
32+
Compact-u16 values are parsed in their complete canonical one-, two-, or
33+
three-byte form. Absolute offsets and transaction lengths are stored as `u32`.
34+
Fallible parsing validates additions, multiplications, ranges, and conversion to
35+
that representation before any unchecked iterator or typed-slice access. Code
36+
using a sanitized view may rely on those validated frame boundaries.
37+
38+
## Address lookup tables
39+
40+
Address lookup tables are unsupported. A transaction containing any lookup
41+
table entry fails sanitization with `TransactionViewError::AddressLookupMismatch`.
42+
A v0 transaction with an empty lookup list remains valid and requires no loaded
43+
addresses.
44+
45+
## Maintenance constraints
46+
47+
- Preserve standard Legacy and v0 wire compatibility for client-produced
48+
transactions.
49+
- Keep V1 and Magicblock framing synchronized; only the version, size, account,
50+
and instruction-trace policies intentionally differ.
51+
- Keep the Magicblock prefix, signed message range, and Engine transaction
52+
composer synchronized.
53+
- Treat the sanitizer as the authoritative boundary for rejecting address
54+
lookup tables.
55+
- Validate new frame offsets before exposing them through unchecked accessors.

‎solana/transaction-view/benches/bytes.rs‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ fn setup() -> Vec<(u16, usize, Vec<u8>)> {
1515
for value in 0..PACKET_DATA_SIZE as u16 {
1616
let short_u16 = ShortU16(value);
1717
let mut buffer = vec![0u8; 16];
18-
let serialized_len = options
19-
.serialized_size(&short_u16)
20-
.expect("Failed to get serialized size");
18+
let serialized_len =
19+
options.serialized_size(&short_u16).expect("Failed to get serialized size");
2120
serialize_into(&mut buffer[..], &short_u16).expect("Serialization failed");
2221
values.push((value, serialized_len as usize, buffer));
2322
}

‎solana/transaction-view/benches/transaction_view.rs‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,7 @@ fn simple_transfers() -> Vec<VersionedTransaction> {
9595
let keypair = Keypair::new();
9696
VersionedTransaction::try_new(
9797
VersionedMessage::Legacy(Message::new_with_blockhash(
98-
&[system_instruction::transfer(
99-
&keypair.pubkey(),
100-
&Pubkey::new_unique(),
101-
1,
102-
)],
98+
&[system_instruction::transfer(&keypair.pubkey(), &Pubkey::new_unique(), 1)],
10399
Some(&keypair.pubkey()),
104100
&Hash::default(),
105101
)),

‎solana/transaction-view/src/bytes.rs‎

Lines changed: 8 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,9 @@ pub fn check_remaining(bytes: &[u8], offset: usize, num_bytes: usize) -> Result<
2828
pub fn read_byte(bytes: &[u8], offset: &mut usize) -> Result<u8> {
2929
// Implicitly checks that the offset is within bounds, no need
3030
// to call `check_remaining` explicitly here.
31-
let value = bytes
32-
.get(*offset)
33-
.copied()
34-
.ok_or(TransactionViewError::ParseError);
35-
*offset = offset.wrapping_add(1);
36-
value
31+
let value = bytes.get(*offset).copied().ok_or(TransactionViewError::ParseError)?;
32+
*offset = offset.checked_add(1).ok_or(TransactionViewError::ParseError)?;
33+
Ok(value)
3734
}
3835

3936
/// Read a byte and advance the offset without any bounds checks.
@@ -69,9 +66,8 @@ pub fn read_compressed_u16(bytes: &[u8], offset: &mut usize) -> Result<u16> {
6966
for i in 0..3 {
7067
// Implicitly checks that the offset is within bounds, no need
7168
// to call check_remaining explicitly here.
72-
let byte = *bytes
73-
.get(offset.wrapping_add(i))
74-
.ok_or(TransactionViewError::ParseError)?;
69+
let index = offset.checked_add(i).ok_or(TransactionViewError::ParseError)?;
70+
let byte = *bytes.get(index).ok_or(TransactionViewError::ParseError)?;
7571
// non-minimal encoding or overflow
7672
if (i > 0 && byte == 0) || (i == 2 && byte > 3) {
7773
return Err(TransactionViewError::ParseError);
@@ -85,46 +81,7 @@ pub fn read_compressed_u16(bytes: &[u8], offset: &mut usize) -> Result<u16> {
8581
}
8682

8783
// if we reach here, it means that all 3 bytes were used
88-
*offset = offset.wrapping_add(3);
89-
Ok(result)
90-
}
91-
92-
/// Domain-specific optimization for reading a compressed u16.
93-
///
94-
/// The compressed u16's are only used for array-lengths in our transaction
95-
/// format. The transaction packet has a maximum size of 1232 bytes.
96-
/// This means that the maximum array length within a **valid** transaction is
97-
/// 1232. This has a minimally encoded length of 2 bytes.
98-
/// Although the encoding scheme allows for more, any arrays with this length
99-
/// would be too large to fit in a packet. This function optimizes for this
100-
/// case, and reads a maximum of 2 bytes.
101-
/// If the buffer is too short or the encoding is invalid, return Err.
102-
/// `offset` is updated to point to the byte after the compressed u16.
103-
///
104-
/// * `bytes` - Slice of bytes to read from.
105-
/// * `offset` - Current offset into `bytes`.
106-
#[inline(always)]
107-
pub fn optimized_read_compressed_u16(bytes: &[u8], offset: &mut usize) -> Result<u16> {
108-
let mut result = 0u16;
109-
110-
// First byte
111-
let byte1 = *bytes.get(*offset).ok_or(TransactionViewError::ParseError)?;
112-
result |= (byte1 & 0x7F) as u16;
113-
if byte1 & 0x80 == 0 {
114-
*offset = offset.wrapping_add(1);
115-
return Ok(result);
116-
}
117-
118-
// Second byte
119-
let byte2 = *bytes
120-
.get(offset.wrapping_add(1))
121-
.ok_or(TransactionViewError::ParseError)?;
122-
if byte2 == 0 || byte2 & 0x80 != 0 {
123-
return Err(TransactionViewError::ParseError); // non-minimal encoding or overflow
124-
}
125-
result |= ((byte2 & 0x7F) as u16) << 7;
126-
*offset = offset.wrapping_add(2);
127-
84+
*offset = offset.checked_add(3).ok_or(TransactionViewError::ParseError)?;
12885
Ok(result)
12986
}
13087

@@ -313,9 +270,8 @@ mod tests {
313270
serialize_into(&mut buffer[..], &short_u16).expect("Serialization failed");
314271

315272
// Use bincode's size calculation to determine the length of the serialized data
316-
let serialized_len = options
317-
.serialized_size(&short_u16)
318-
.expect("Failed to get serialized size");
273+
let serialized_len =
274+
options.serialized_size(&short_u16).expect("Failed to get serialized size");
319275

320276
// Reset offset
321277
offset = 0;
@@ -351,55 +307,6 @@ mod tests {
351307
assert!(read_compressed_u16(&[0x81, 0x80, 0x00], &mut 0).is_err());
352308
}
353309

354-
#[test]
355-
fn test_optimized_read_compressed_u16() {
356-
let mut buffer = [0u8; 1024];
357-
let options = DefaultOptions::new().with_fixint_encoding(); // Ensure fixed-int encoding
358-
359-
// Test all possible u16 values under the packet length
360-
for value in 0..=PACKET_DATA_SIZE as u16 {
361-
let mut offset;
362-
let short_u16 = ShortU16(value);
363-
364-
// Serialize the value into the buffer
365-
serialize_into(&mut buffer[..], &short_u16).expect("Serialization failed");
366-
367-
// Use bincode's size calculation to determine the length of the serialized data
368-
let serialized_len = options
369-
.serialized_size(&short_u16)
370-
.expect("Failed to get serialized size");
371-
372-
// Reset offset
373-
offset = 0;
374-
375-
// Read the value back using unchecked_read_u16_compressed
376-
let read_value = optimized_read_compressed_u16(&buffer, &mut offset);
377-
378-
// Assert that the read value matches the original value
379-
assert_eq!(read_value, Ok(value), "Value mismatch for: {value}");
380-
381-
// Assert that the offset matches the serialized length
382-
assert_eq!(
383-
offset, serialized_len as usize,
384-
"Offset mismatch for: {value}"
385-
);
386-
}
387-
388-
// Test bounds.
389-
// All 0s => 0
390-
assert_eq!(Ok(0), optimized_read_compressed_u16(&[0; 3], &mut 0));
391-
// Overflow
392-
assert!(optimized_read_compressed_u16(&[0xFF, 0xFF, 0x04], &mut 0).is_err());
393-
assert!(optimized_read_compressed_u16(&[0xFF, 0x80], &mut 0).is_err());
394-
395-
// overflow errors
396-
assert!(optimized_read_compressed_u16(&[u8::MAX; 1], &mut 0).is_err());
397-
assert!(optimized_read_compressed_u16(&[u8::MAX; 2], &mut 0).is_err());
398-
399-
// Minimal encoding checks
400-
assert!(optimized_read_compressed_u16(&[0x81, 0x00], &mut 0).is_err());
401-
}
402-
403310
#[test]
404311
fn test_advance_offset_for_array() {
405312
#[repr(C)]

0 commit comments

Comments
 (0)