Skip to content

Commit a3675f1

Browse files
authored
Fix cbor serialization overhead (#2064)
1 parent 15c8d13 commit a3675f1

File tree

5 files changed

+264
-5
lines changed

5 files changed

+264
-5
lines changed

aggregation_mode/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.

crates/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.

crates/batcher/src/types/batch_queue.rs

Lines changed: 205 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -215,12 +215,16 @@ fn calculate_fee_per_proof(batch_len: usize, gas_price: U256, constant_gas_cost:
215215

216216
#[cfg(test)]
217217
mod test {
218-
use aligned_sdk::common::constants::DEFAULT_CONSTANT_GAS_COST;
219-
use aligned_sdk::common::types::ProvingSystemId;
220-
use aligned_sdk::common::types::VerificationData;
221-
use ethers::types::Address;
218+
use crate::types::batch_queue::extract_batch_directly;
222219

223-
use super::*;
220+
use super::{BatchQueue, BatchQueueEntry, BatchQueueEntryPriority};
221+
use aligned_sdk::common::constants::DEFAULT_CONSTANT_GAS_COST;
222+
use aligned_sdk::common::types::{
223+
NoncedVerificationData, ProvingSystemId, VerificationData, VerificationDataCommitment,
224+
};
225+
use aligned_sdk::communication::serialization::cbor_serialize;
226+
use ethers::types::{Address, Signature, U256};
227+
use std::fs;
224228

225229
#[test]
226230
fn batch_finalization_algorithm_works_from_same_sender() {
@@ -973,4 +977,200 @@ mod test {
973977
max_fee_1
974978
);
975979
}
980+
981+
#[test]
982+
fn test_batch_size_limit_enforcement_with_real_sp1_proofs() {
983+
let proof_generator_addr = Address::random();
984+
let payment_service_addr = Address::random();
985+
let chain_id = U256::from(42);
986+
let dummy_signature = Signature {
987+
r: U256::from(1),
988+
s: U256::from(2),
989+
v: 3,
990+
};
991+
992+
// Load actual SP1 proof files
993+
let proof_path = "../../scripts/test_files/sp1/sp1_fibonacci_5_0_0.proof";
994+
let elf_path = "../../scripts/test_files/sp1/sp1_fibonacci_5_0_0.elf";
995+
let pub_input_path = "../../scripts/test_files/sp1/sp1_fibonacci_5_0_0.pub";
996+
997+
let proof_data = match fs::read(proof_path) {
998+
Ok(data) => data,
999+
Err(_) => return, // Skip test if files not found
1000+
};
1001+
1002+
let elf_data = match fs::read(elf_path) {
1003+
Ok(data) => data,
1004+
Err(_) => return,
1005+
};
1006+
1007+
let pub_input_data = match fs::read(pub_input_path) {
1008+
Ok(data) => data,
1009+
Err(_) => return,
1010+
};
1011+
1012+
let verification_data = VerificationData {
1013+
proving_system: ProvingSystemId::SP1,
1014+
proof: proof_data,
1015+
pub_input: Some(pub_input_data),
1016+
verification_key: None,
1017+
vm_program_code: Some(elf_data),
1018+
proof_generator_addr,
1019+
};
1020+
1021+
// Create 10 entries using the same SP1 proof data
1022+
let mut batch_queue = BatchQueue::new();
1023+
let max_fee = U256::from(1_000_000_000_000_000u128);
1024+
1025+
for i in 0..10 {
1026+
let sender_addr = Address::random();
1027+
let nonce = U256::from(i + 1);
1028+
1029+
let nonced_verification_data = NoncedVerificationData::new(
1030+
verification_data.clone(),
1031+
nonce,
1032+
max_fee,
1033+
chain_id,
1034+
payment_service_addr,
1035+
);
1036+
1037+
let vd_commitment: VerificationDataCommitment = nonced_verification_data.clone().into();
1038+
let entry = BatchQueueEntry::new_for_testing(
1039+
nonced_verification_data,
1040+
vd_commitment,
1041+
dummy_signature,
1042+
sender_addr,
1043+
);
1044+
let batch_priority = BatchQueueEntryPriority::new(max_fee, nonce);
1045+
batch_queue.push(entry, batch_priority);
1046+
}
1047+
1048+
// Test with a 5MB batch size limit
1049+
let batch_size_limit = 5_000_000; // 5MB
1050+
let gas_price = U256::from(1);
1051+
1052+
let finalized_batch = extract_batch_directly(
1053+
&mut batch_queue,
1054+
gas_price,
1055+
batch_size_limit,
1056+
50, // max proof qty
1057+
DEFAULT_CONSTANT_GAS_COST,
1058+
)
1059+
.unwrap();
1060+
1061+
// Verify the finalized batch respects the size limit
1062+
let finalized_verification_data: Vec<VerificationData> = finalized_batch
1063+
.iter()
1064+
.map(|entry| entry.nonced_verification_data.verification_data.clone())
1065+
.collect();
1066+
1067+
let finalized_serialized = cbor_serialize(&finalized_verification_data).unwrap();
1068+
let finalized_actual_size = finalized_serialized.len();
1069+
1070+
// Assert the batch respects the limit
1071+
assert!(
1072+
finalized_actual_size <= batch_size_limit,
1073+
"Finalized batch size {} exceeds limit {}",
1074+
finalized_actual_size,
1075+
batch_size_limit
1076+
);
1077+
1078+
// Verify some entries were included (not empty batch)
1079+
assert!(!finalized_batch.is_empty(), "Batch should not be empty");
1080+
1081+
// Verify not all entries were included (some should be rejected due to size limit)
1082+
assert!(
1083+
finalized_batch.len() < 10,
1084+
"Batch should not include all entries due to size limit"
1085+
);
1086+
}
1087+
1088+
#[test]
1089+
fn test_cbor_size_upper_bound_accuracy() {
1090+
let proof_generator_addr = Address::random();
1091+
let payment_service_addr = Address::random();
1092+
let chain_id = U256::from(42);
1093+
1094+
// Load actual SP1 proof files
1095+
let proof_path = "../../scripts/test_files/sp1/sp1_fibonacci_5_0_0.proof";
1096+
let elf_path = "../../scripts/test_files/sp1/sp1_fibonacci_5_0_0.elf";
1097+
let pub_input_path = "../../scripts/test_files/sp1/sp1_fibonacci_5_0_0.pub";
1098+
1099+
let proof_data = match fs::read(proof_path) {
1100+
Ok(data) => data,
1101+
Err(_) => return, // Skip test if files not found
1102+
};
1103+
1104+
let elf_data = match fs::read(elf_path) {
1105+
Ok(data) => data,
1106+
Err(_) => return,
1107+
};
1108+
1109+
let pub_input_data = match fs::read(pub_input_path) {
1110+
Ok(data) => data,
1111+
Err(_) => return,
1112+
};
1113+
1114+
let verification_data = VerificationData {
1115+
proving_system: ProvingSystemId::SP1,
1116+
proof: proof_data,
1117+
pub_input: Some(pub_input_data),
1118+
verification_key: None,
1119+
vm_program_code: Some(elf_data),
1120+
proof_generator_addr,
1121+
};
1122+
1123+
let nonced_verification_data = NoncedVerificationData::new(
1124+
verification_data.clone(),
1125+
U256::from(1),
1126+
U256::from(1_000_000_000_000_000u128),
1127+
chain_id,
1128+
payment_service_addr,
1129+
);
1130+
1131+
// Test cbor_size_upper_bound() accuracy
1132+
let estimated_size = nonced_verification_data.cbor_size_upper_bound();
1133+
1134+
// Compare with actual CBOR serialization of the full NoncedVerificationData
1135+
let actual_nonced_serialized = cbor_serialize(&nonced_verification_data).unwrap();
1136+
let actual_nonced_size = actual_nonced_serialized.len();
1137+
1138+
// Also test the inner VerificationData for additional validation
1139+
let actual_inner_serialized = cbor_serialize(&verification_data).unwrap();
1140+
let actual_inner_size = actual_inner_serialized.len();
1141+
1142+
// Verify CBOR encodes binary data efficiently (with serde_bytes fix), this misses some overhead but the proof is big enough as to not matter
1143+
1144+
let raw_total = verification_data.proof.len()
1145+
+ verification_data.vm_program_code.as_ref().unwrap().len()
1146+
+ verification_data.pub_input.as_ref().unwrap().len();
1147+
1148+
let cbor_efficiency_ratio = actual_inner_size as f64 / raw_total as f64;
1149+
1150+
// With serde_bytes, CBOR should be very efficient (close to 1.0x)
1151+
assert!(
1152+
cbor_efficiency_ratio < 1.01,
1153+
"CBOR serialization should be efficient with serde_bytes. Ratio: {:.3}x",
1154+
cbor_efficiency_ratio
1155+
);
1156+
1157+
// Verify CBOR encodes binary data efficiently with serde_bytes
1158+
// Should be close to 1.0x overhead (raw data size vs CBOR size)
1159+
1160+
// The estimation should be an upper bound
1161+
assert!(
1162+
estimated_size >= actual_nonced_size,
1163+
"cbor_size_upper_bound() should be an upper bound. Estimated: {}, Actual: {}",
1164+
estimated_size,
1165+
actual_nonced_size
1166+
);
1167+
1168+
// The estimation should also be reasonable (not wildly over-estimated)
1169+
let estimation_overhead = estimated_size as f64 / actual_nonced_size as f64;
1170+
assert!(
1171+
estimation_overhead < 1.1,
1172+
"Estimation should be reasonable, not wildly over-estimated. Overhead: {:.3}x",
1173+
estimation_overhead
1174+
);
1175+
}
9761176
}

crates/sdk/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ tokio = { version = "1.37.0", features = [
1919
] }
2020
lambdaworks-crypto = { git = "https://github.com/lambdaclass/lambdaworks.git", rev = "5f8f2cfcc8a1a22f77e8dff2d581f1166eefb80b", features = ["serde"]}
2121
serde = { version = "1.0.201", features = ["derive"] }
22+
serde_bytes = "0.11"
2223
sha3 = { version = "0.10.8" }
2324
url = "2.5.0"
2425
hex = "0.4.3"

crates/sdk/src/common/types.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,28 @@ impl Display for ProvingSystemId {
6565
#[derive(Debug, Serialize, Deserialize, Clone)]
6666
pub struct VerificationData {
6767
pub proving_system: ProvingSystemId,
68+
#[serde(with = "serde_bytes")]
6869
pub proof: Vec<u8>,
70+
#[serde(
71+
default,
72+
skip_serializing_if = "Option::is_none",
73+
deserialize_with = "deserialize_option_bytes",
74+
serialize_with = "serialize_option_bytes"
75+
)]
6976
pub pub_input: Option<Vec<u8>>,
77+
#[serde(
78+
default,
79+
skip_serializing_if = "Option::is_none",
80+
deserialize_with = "deserialize_option_bytes",
81+
serialize_with = "serialize_option_bytes"
82+
)]
7083
pub verification_key: Option<Vec<u8>>,
84+
#[serde(
85+
default,
86+
skip_serializing_if = "Option::is_none",
87+
deserialize_with = "deserialize_option_bytes",
88+
serialize_with = "serialize_option_bytes"
89+
)]
7190
pub vm_program_code: Option<Vec<u8>>,
7291
pub proof_generator_addr: Address,
7392
}
@@ -504,6 +523,25 @@ impl Network {
504523
}
505524
}
506525

526+
// Helper functions for serializing Option<Vec<u8>> with serde_bytes
527+
fn serialize_option_bytes<S>(value: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
528+
where
529+
S: serde::Serializer,
530+
{
531+
match value {
532+
Some(bytes) => serde_bytes::serialize(bytes, serializer),
533+
None => serializer.serialize_none(),
534+
}
535+
}
536+
537+
fn deserialize_option_bytes<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
538+
where
539+
D: serde::Deserializer<'de>,
540+
{
541+
let opt: Option<serde_bytes::ByteBuf> = Option::deserialize(deserializer)?;
542+
Ok(opt.map(|buf| buf.into_vec()))
543+
}
544+
507545
#[cfg(test)]
508546
mod tests {
509547
use ethers::signers::LocalWallet;

0 commit comments

Comments
 (0)