reth_rpc/validation/
blob_cache.rs1use super::ValidationApiError;
14use alloy_consensus::EnvKzgSettings;
15use alloy_eips::eip7594::{BlobTransactionSidecarEip7594, CELLS_PER_EXT_BLOB};
16use alloy_primitives::B256;
17use alloy_rpc_types_engine::BlobsBundleV2;
18use parking_lot::Mutex;
19use std::collections::VecDeque;
20
21const VALIDATED_BLOB_CACHE_CAPACITY: usize = 15;
23
24#[derive(Debug, Default)]
26pub(super) struct BlobValidationCache {
27 entries: Mutex<VecDeque<BlobTransactionSidecarEip7594>>,
29}
30
31impl BlobValidationCache {
32 pub(super) fn validate(
34 &self,
35 mut bundle: BlobsBundleV2,
36 ) -> Result<Vec<B256>, ValidationApiError> {
37 if bundle.blobs.len() != bundle.commitments.len() ||
38 bundle.blobs.len().checked_mul(CELLS_PER_EXT_BLOB) != Some(bundle.proofs.len())
39 {
40 return Err(ValidationApiError::InvalidBlobsBundle)
41 }
42 if bundle.blobs.is_empty() {
43 return Ok(Vec::new())
44 }
45
46 let versioned_hashes = bundle.versioned_hashes();
47 let hits = {
48 let entries = self.entries.lock();
49 (0..bundle.blobs.len())
50 .map(|index| {
51 let proofs = &bundle.proofs
52 [index * CELLS_PER_EXT_BLOB..(index + 1) * CELLS_PER_EXT_BLOB];
53 entries.iter().any(|cached| {
54 cached.commitments[0] == bundle.commitments[index] &&
55 cached.cell_proofs == proofs &&
56 cached.blobs[0] == bundle.blobs[index]
57 })
58 })
59 .collect::<Vec<_>>()
60 };
61 if hits.iter().all(|hit| *hit) {
62 return Ok(versioned_hashes)
63 }
64
65 let mut cached = BlobsBundleV2::empty();
66 let mut missing = BlobsBundleV2::empty();
67 for hit in hits {
68 let partition = if hit { &mut cached } else { &mut missing };
69 partition.blobs.extend(bundle.blobs.drain(..1));
71 partition.commitments.extend(bundle.commitments.drain(..1));
72 partition.proofs.extend(bundle.proofs.drain(..CELLS_PER_EXT_BLOB));
73 }
74 drop(cached);
75
76 let sidecar =
77 missing.try_into_sidecar().map_err(|_| ValidationApiError::InvalidBlobsBundle)?;
78 let missing_hashes = sidecar.versioned_hashes().collect::<Vec<_>>();
79 sidecar.validate(&missing_hashes, EnvKzgSettings::default().get())?;
80
81 for index in 0..sidecar.blobs.len() {
82 let proofs =
83 &sidecar.cell_proofs[index * CELLS_PER_EXT_BLOB..(index + 1) * CELLS_PER_EXT_BLOB];
84 self.insert(BlobTransactionSidecarEip7594::new(
85 vec![sidecar.blobs[index]],
86 vec![sidecar.commitments[index]],
87 proofs.to_vec(),
88 ));
89 }
90
91 Ok(versioned_hashes)
92 }
93
94 fn insert(&self, sidecar: BlobTransactionSidecarEip7594) {
95 let mut entries = self.entries.lock();
96 if entries
97 .iter()
98 .any(|cached| cached.commitments == sidecar.commitments && cached == &sidecar)
99 {
100 return;
101 }
102 if entries.len() == VALIDATED_BLOB_CACHE_CAPACITY {
103 entries.pop_front();
104 }
105 entries.push_back(sidecar);
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use alloy_consensus::{Blob, BlobTransactionSidecar, Bytes48, SidecarBuilder, SimpleCoder};
113 use alloy_rpc_types_engine::BlobsBundleV1;
114
115 fn valid_v1_bundle(data: &[u8]) -> BlobsBundleV1 {
116 let mut builder = SidecarBuilder::<SimpleCoder>::new();
117 builder.ingest(data);
118 BlobsBundleV1::new([builder.build::<BlobTransactionSidecar>().unwrap()])
119 }
120
121 fn valid_v2_bundle(data: &[u8]) -> BlobsBundleV2 {
122 valid_v1_bundle(data).try_into_v2().unwrap()
123 }
124
125 #[test]
126 fn mixed_bundle_validates_only_missing_blobs() {
127 let cache = BlobValidationCache::default();
128 let mut bundle = valid_v2_bundle(b"first blob");
129 let second = valid_v2_bundle(b"second blob");
130 cache.validate(valid_v2_bundle(b"first blob")).unwrap();
131
132 bundle.blobs.extend(second.blobs);
133 bundle.commitments.extend(second.commitments);
134 bundle.proofs.extend(second.proofs);
135 assert_eq!(cache.validate(bundle.clone()).unwrap(), bundle.versioned_hashes());
136 assert_eq!(cache.entries.lock().len(), 2);
137 }
138
139 #[test]
140 fn v2_reuses_only_exact_validated_blobs() {
141 let cache = BlobValidationCache::default();
142 let hashes = valid_v2_bundle(b"peer das blob").versioned_hashes();
143
144 assert_eq!(cache.validate(valid_v2_bundle(b"peer das blob")).unwrap(), hashes);
145 assert_eq!(cache.validate(valid_v2_bundle(b"peer das blob")).unwrap(), hashes);
146 assert_eq!(cache.entries.lock().len(), 1);
147
148 let mut changed_blob = valid_v2_bundle(b"peer das blob");
149 changed_blob.blobs[0].0[0] ^= 1;
150 assert!(cache.validate(changed_blob).is_err());
151
152 let mut changed_proof = valid_v2_bundle(b"peer das blob");
153 changed_proof.proofs[0].0[0] ^= 1;
154 assert!(cache.validate(changed_proof).is_err());
155 assert_eq!(cache.entries.lock().len(), 1);
156 }
157
158 #[test]
159 fn validates_uncached_blob_even_when_another_hits() {
160 let cache = BlobValidationCache::default();
161 let mut bundle = valid_v2_bundle(b"first blob");
162 let mut second = valid_v2_bundle(b"second blob");
163 cache.validate(valid_v2_bundle(b"first blob")).unwrap();
164
165 second.proofs[0].0[0] ^= 1;
166 bundle.blobs.extend(second.blobs);
167 bundle.commitments.extend(second.commitments);
168 bundle.proofs.extend(second.proofs);
169 assert!(cache.validate(bundle).is_err());
170 assert_eq!(cache.entries.lock().len(), 1);
171 }
172
173 #[test]
174 fn rejects_incomplete_bundles_without_changing_cache() {
175 let cache = BlobValidationCache::default();
176 let mut v2 = valid_v2_bundle(b"blob");
177 v2.proofs.pop();
178 assert!(matches!(cache.validate(v2), Err(ValidationApiError::InvalidBlobsBundle)));
179 assert!(cache.entries.lock().is_empty());
180 }
181
182 #[test]
183 fn empty_bundle_still_checks_lengths() {
184 let cache = BlobValidationCache::default();
185 assert!(cache.validate(BlobsBundleV2::empty()).unwrap().is_empty());
186
187 let mut malformed = BlobsBundleV2::empty();
188 malformed.proofs.push(Bytes48::ZERO);
189 assert!(matches!(cache.validate(malformed), Err(ValidationApiError::InvalidBlobsBundle)));
190 }
191
192 #[test]
193 fn retains_only_fifteen_recent_blobs() {
194 let cache = BlobValidationCache::default();
195 let blob = Blob::default();
196 for index in 0..=VALIDATED_BLOB_CACHE_CAPACITY {
197 cache.insert(BlobTransactionSidecarEip7594::new(
198 vec![blob],
199 vec![Bytes48::repeat_byte(index as u8)],
200 vec![Bytes48::ZERO; CELLS_PER_EXT_BLOB],
201 ));
202 }
203
204 let entries = cache.entries.lock();
205 assert_eq!(entries.len(), VALIDATED_BLOB_CACHE_CAPACITY);
206 assert_eq!(entries.front().unwrap().commitments[0], Bytes48::repeat_byte(1));
207 }
208}