use std::{
collections::{HashMap, HashSet},
time::Instant,
};
use alloy_primitives::{Address, B256, Bytes, FixedBytes, Keccak256, Log as PrimitiveLog};
use alloy_rpc_types_eth::Log;
use tokio::sync::{mpsc, oneshot};
use super::{
BaseFlashblockBase, FlashblockContentCommitment, FlashblockIngressTiming, FlashblockRef,
ProviderRef, deserialize_optional_rpc_u64, flashblock_content_hash,
flashblock_transaction_hashes, non_placeholder_hash,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct RawJsonFlashblocksLimits {
pub max_frame_bytes: usize,
pub max_flashblocks_per_payload: usize,
pub max_transactions_per_payload: usize,
pub max_logs_per_payload: usize,
}
impl Default for RawJsonFlashblocksLimits {
fn default() -> Self {
Self {
max_frame_bytes: 16 * 1024 * 1024,
max_flashblocks_per_payload: 64,
max_transactions_per_payload: 50_000,
max_logs_per_payload: 200_000,
}
}
}
impl RawJsonFlashblocksLimits {
fn validate(self) -> Result<(), RawJsonFlashblocksError> {
if self.max_frame_bytes == 0 {
return Err(RawJsonFlashblocksError::InvalidLimits(
"max_frame_bytes must be greater than zero",
));
}
if self.max_flashblocks_per_payload == 0 {
return Err(RawJsonFlashblocksError::InvalidLimits(
"max_flashblocks_per_payload must be greater than zero",
));
}
if self.max_transactions_per_payload == 0 {
return Err(RawJsonFlashblocksError::InvalidLimits(
"max_transactions_per_payload must be greater than zero",
));
}
if self.max_logs_per_payload == 0 {
return Err(RawJsonFlashblocksError::InvalidLimits(
"max_logs_per_payload must be greater than zero",
));
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct FlashblockSnapshot {
pub flashblock: FlashblockRef,
pub logs: Vec<Log>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum FlashblockInvalidationReason {
IndexGap,
ConflictingDuplicate,
MissingInitialIndex,
SourceReset,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct FlashblockInvalidation {
pub provider: ProviderRef,
pub payload_id: FixedBytes<8>,
pub reason: FlashblockInvalidationReason,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum FlashblockUpdate {
Snapshot(Box<FlashblockSnapshot>),
Invalidated(FlashblockInvalidation),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TimedFlashblockUpdate {
update: FlashblockUpdate,
source_ingress_millis: u64,
}
impl TimedFlashblockUpdate {
const fn new(update: FlashblockUpdate, source_ingress_millis: u64) -> Self {
Self {
update,
source_ingress_millis,
}
}
pub const fn update(&self) -> &FlashblockUpdate {
&self.update
}
pub const fn source_ingress_millis(&self) -> u64 {
self.source_ingress_millis
}
pub fn into_update(self) -> FlashblockUpdate {
self.update
}
}
impl FlashblockUpdate {
pub const fn provider(&self) -> &ProviderRef {
match self {
Self::Snapshot(snapshot) => &snapshot.flashblock.provider,
Self::Invalidated(invalidation) => &invalidation.provider,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum FlashblockUpdateChannelError {
#[error("Flashblock update came from an unexpected provider endpoint")]
UnexpectedEndpoint,
#[error("Flashblock update channel is full")]
Full,
#[error("Flashblock update channel is closed")]
Closed,
#[error("Flashblock update was rejected by the subscriber")]
Rejected,
}
#[derive(Debug)]
#[must_use = "await wait() to observe the subscriber validation verdict"]
pub struct FlashblockUpdateAcknowledgement {
receiver: oneshot::Receiver<Result<(), FlashblockUpdateChannelError>>,
}
impl FlashblockUpdateAcknowledgement {
pub async fn wait(self) -> Result<(), FlashblockUpdateChannelError> {
self.receiver
.await
.unwrap_or(Err(FlashblockUpdateChannelError::Closed))
}
}
pub(crate) struct QueuedFlashblockUpdate {
pub(crate) update: FlashblockUpdate,
pub(crate) timing: FlashblockIngressTiming,
pub(crate) acknowledgement: oneshot::Sender<Result<(), FlashblockUpdateChannelError>>,
}
impl QueuedFlashblockUpdate {
fn new(
update: FlashblockUpdate,
timing: FlashblockIngressTiming,
) -> (Self, FlashblockUpdateAcknowledgement) {
let (acknowledgement, receiver) = oneshot::channel();
(
Self {
update,
timing,
acknowledgement,
},
FlashblockUpdateAcknowledgement { receiver },
)
}
}
#[derive(Clone, Debug)]
pub struct FlashblockUpdateSender {
provider: ProviderRef,
sender: mpsc::Sender<QueuedFlashblockUpdate>,
}
impl FlashblockUpdateSender {
pub(crate) const fn new(
provider: ProviderRef,
sender: mpsc::Sender<QueuedFlashblockUpdate>,
) -> Self {
Self { provider, sender }
}
pub const fn provider(&self) -> &ProviderRef {
&self.provider
}
pub async fn send(&self, update: FlashblockUpdate) -> Result<(), FlashblockUpdateChannelError> {
self.send_with_ingress(update, FlashblockIngressTiming::new(Instant::now()))
.await
}
pub async fn send_with_ingress(
&self,
update: FlashblockUpdate,
timing: FlashblockIngressTiming,
) -> Result<(), FlashblockUpdateChannelError> {
self.validate_endpoint(&update)?;
let (queued, acknowledgement) = QueuedFlashblockUpdate::new(update, timing);
self.sender
.send(queued)
.await
.map_err(|_| FlashblockUpdateChannelError::Closed)?;
acknowledgement.wait().await
}
pub fn try_send(
&self,
update: FlashblockUpdate,
) -> Result<FlashblockUpdateAcknowledgement, FlashblockUpdateChannelError> {
self.try_send_with_ingress(update, FlashblockIngressTiming::new(Instant::now()))
}
pub fn try_send_with_ingress(
&self,
update: FlashblockUpdate,
timing: FlashblockIngressTiming,
) -> Result<FlashblockUpdateAcknowledgement, FlashblockUpdateChannelError> {
self.validate_endpoint(&update)?;
let (queued, acknowledgement) = QueuedFlashblockUpdate::new(update, timing);
self.sender.try_send(queued).map_err(|error| match error {
mpsc::error::TrySendError::Full(_) => FlashblockUpdateChannelError::Full,
mpsc::error::TrySendError::Closed(_) => FlashblockUpdateChannelError::Closed,
})?;
Ok(acknowledgement)
}
fn validate_endpoint(
&self,
update: &FlashblockUpdate,
) -> Result<(), FlashblockUpdateChannelError> {
if update.provider().endpoint != self.provider.endpoint {
return Err(FlashblockUpdateChannelError::UnexpectedEndpoint);
}
Ok(())
}
}
pub(crate) fn flashblock_update_channel(
provider: ProviderRef,
capacity: usize,
) -> (
FlashblockUpdateSender,
mpsc::Receiver<QueuedFlashblockUpdate>,
) {
let (sender, receiver) = mpsc::channel(capacity);
(FlashblockUpdateSender::new(provider, sender), receiver)
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum RawJsonFlashblocksError {
#[error("invalid raw JSON Flashblocks limits: {0}")]
InvalidLimits(&'static str),
#[error("invalid raw JSON Flashblocks source transition: {0}")]
InvalidSourceTransition(&'static str),
#[error("raw JSON Flashblocks frame exceeds the configured byte limit")]
FrameTooLarge,
#[error("invalid raw JSON Flashblocks payload: {0}")]
InvalidPayload(String),
#[error("raw JSON Flashblocks payload exceeds the configured {0} limit")]
ResourceExhausted(&'static str),
}
#[derive(Clone, Debug)]
pub struct RawJsonFlashblocksAdapter {
provider: ProviderRef,
limits: RawJsonFlashblocksLimits,
active: Option<RawPayloadState>,
ignored_payload: Option<FixedBytes<8>>,
}
impl RawJsonFlashblocksAdapter {
pub fn new(provider: ProviderRef) -> Self {
Self {
provider,
limits: RawJsonFlashblocksLimits::default(),
active: None,
ignored_payload: None,
}
}
pub fn with_limits(
provider: ProviderRef,
limits: RawJsonFlashblocksLimits,
) -> Result<Self, RawJsonFlashblocksError> {
limits.validate()?;
Ok(Self {
provider,
limits,
active: None,
ignored_payload: None,
})
}
pub const fn provider(&self) -> &ProviderRef {
&self.provider
}
pub const fn limits(&self) -> RawJsonFlashblocksLimits {
self.limits
}
pub fn reset(
&mut self,
provider: ProviderRef,
) -> Result<Option<FlashblockUpdate>, RawJsonFlashblocksError> {
if provider.endpoint != self.provider.endpoint {
return Err(RawJsonFlashblocksError::InvalidSourceTransition(
"reset cannot change the configured endpoint identity",
));
}
if provider.generation <= self.provider.generation {
return Err(RawJsonFlashblocksError::InvalidSourceTransition(
"reset requires a strictly newer provider generation",
));
}
let invalidation = self.active.take().map(|active| {
FlashblockUpdate::Invalidated(FlashblockInvalidation {
provider: self.provider.clone(),
payload_id: active.payload_id,
reason: FlashblockInvalidationReason::SourceReset,
})
});
self.provider = provider;
self.ignored_payload = None;
Ok(invalidation)
}
pub fn ingest_json(
&mut self,
frame: &[u8],
) -> Result<Option<FlashblockUpdate>, RawJsonFlashblocksError> {
let payload = self.decode_json(frame)?;
self.ingest(payload)
}
fn decode_json(&self, frame: &[u8]) -> Result<RawFlashblockPayload, RawJsonFlashblocksError> {
if frame.len() > self.limits.max_frame_bytes {
return Err(RawJsonFlashblocksError::FrameTooLarge);
}
let payload: RawFlashblockPayload = serde_json::from_slice(frame)
.map_err(|error| RawJsonFlashblocksError::InvalidPayload(error.to_string()))?;
self.validate_index(payload.index)?;
Ok(payload)
}
fn validate_index(&self, index: u64) -> Result<(), RawJsonFlashblocksError> {
if usize::try_from(index)
.ok()
.is_none_or(|index| index >= self.limits.max_flashblocks_per_payload)
{
return Err(RawJsonFlashblocksError::ResourceExhausted(
"Flashblock index",
));
}
Ok(())
}
fn ingest(
&mut self,
payload: RawFlashblockPayload,
) -> Result<Option<FlashblockUpdate>, RawJsonFlashblocksError> {
self.validate_index(payload.index)?;
if self.ignored_payload == Some(payload.payload_id) {
return Ok(None);
}
let begins_new_payload = self
.active
.as_ref()
.is_none_or(|active| active.payload_id != payload.payload_id);
if begins_new_payload {
if payload.index != 0 {
let invalidated_payload = self
.active
.take()
.map_or(payload.payload_id, |active| active.payload_id);
self.ignored_payload = Some(payload.payload_id);
return Ok(Some(self.invalidation(
invalidated_payload,
FlashblockInvalidationReason::MissingInitialIndex,
)));
}
let base = payload.base.clone().ok_or_else(|| {
RawJsonFlashblocksError::InvalidPayload("index zero omitted its base header".into())
})?;
if let Some(metadata_number) = payload.metadata.block_number
&& metadata_number != base.block_number
{
return Err(RawJsonFlashblocksError::InvalidPayload(
"base and metadata block numbers disagree".into(),
));
}
let previous_active = self.active.take();
let previous_ignored_payload = self.ignored_payload.take();
self.active = Some(RawPayloadState {
payload_id: payload.payload_id,
base,
last_index: None,
cumulative_transactions: Vec::new(),
transaction_set: HashSet::new(),
next_log_index: 0,
cumulative_logs: 0,
index_commitments: HashMap::new(),
});
let result = self.ingest_active(payload);
if result.is_err() {
self.active = previous_active;
self.ignored_payload = previous_ignored_payload;
}
return result;
}
self.ingest_active(payload)
}
fn ingest_active(
&mut self,
payload: RawFlashblockPayload,
) -> Result<Option<FlashblockUpdate>, RawJsonFlashblocksError> {
let active = self.active.as_mut().expect("new payload initialized above");
if payload
.metadata
.block_number
.is_some_and(|number| number != active.base.block_number)
|| payload
.base
.as_ref()
.is_some_and(|base| base != &active.base)
{
return Err(RawJsonFlashblocksError::InvalidPayload(
"base header or metadata block numbers disagree with the active payload".into(),
));
}
let delta_transactions = flashblock_transaction_hashes(&payload.diff.transactions)
.map_err(|error| RawJsonFlashblocksError::InvalidPayload(error.to_string()))?;
let receipt_hashes = payload
.metadata
.receipts
.keys()
.copied()
.collect::<HashSet<_>>();
let transaction_hashes = delta_transactions.iter().copied().collect::<HashSet<_>>();
if transaction_hashes.len() != delta_transactions.len() {
return Err(RawJsonFlashblocksError::InvalidPayload(
"the transaction delta contains a duplicate hash".into(),
));
}
if receipt_hashes != transaction_hashes {
return Err(RawJsonFlashblocksError::InvalidPayload(
"receipt-map membership disagrees with the transaction delta".into(),
));
}
let commitment = raw_payload_commitment(&payload, &delta_transactions);
if let Some(previous) = active.index_commitments.get(&payload.index) {
if *previous == commitment {
return Ok(None);
}
let payload_id = active.payload_id;
self.active = None;
self.ignored_payload = Some(payload_id);
return Ok(Some(self.invalidation(
payload_id,
FlashblockInvalidationReason::ConflictingDuplicate,
)));
}
let expected_index = active.last_index.map_or(0, |index| index.saturating_add(1));
if payload.index != expected_index {
let payload_id = active.payload_id;
self.active = None;
self.ignored_payload = Some(payload_id);
return Ok(Some(
self.invalidation(payload_id, FlashblockInvalidationReason::IndexGap),
));
}
if active
.cumulative_transactions
.len()
.saturating_add(delta_transactions.len())
> self.limits.max_transactions_per_payload
{
return Err(RawJsonFlashblocksError::ResourceExhausted(
"transaction count",
));
}
if delta_transactions
.iter()
.any(|hash| active.transaction_set.contains(hash))
{
return Err(RawJsonFlashblocksError::InvalidPayload(
"a transaction appeared in more than one indexed delta".into(),
));
}
let transaction_offset = active.cumulative_transactions.len();
let mut logs = Vec::new();
for (delta_index, transaction_hash) in delta_transactions.iter().enumerate() {
let receipt = payload
.metadata
.receipts
.get(transaction_hash)
.expect("receipt membership checked above");
let transaction_index =
u64::try_from(transaction_offset.saturating_add(delta_index))
.map_err(|_| RawJsonFlashblocksError::ResourceExhausted("transaction index"))?;
for raw_log in &receipt.logs {
if active.cumulative_logs.saturating_add(logs.len())
>= self.limits.max_logs_per_payload
{
return Err(RawJsonFlashblocksError::ResourceExhausted("log count"));
}
let inner = PrimitiveLog::new(
raw_log.address,
raw_log.topics.clone(),
raw_log.data.clone(),
)
.ok_or_else(|| {
RawJsonFlashblocksError::InvalidPayload(
"receipt log contains more than four topics".into(),
)
})?;
let log_index = active
.next_log_index
.checked_add(
u64::try_from(logs.len())
.map_err(|_| RawJsonFlashblocksError::ResourceExhausted("log index"))?,
)
.ok_or(RawJsonFlashblocksError::ResourceExhausted("log index"))?;
logs.push(Log {
inner,
block_hash: None,
block_number: Some(active.base.block_number),
block_timestamp: Some(active.base.timestamp),
transaction_hash: Some(*transaction_hash),
transaction_index: Some(transaction_index),
log_index: Some(log_index),
removed: false,
});
}
}
let mut cumulative_transactions = active.cumulative_transactions.clone();
cumulative_transactions.extend(delta_transactions.iter().copied());
let partial_block_hash = non_placeholder_hash(payload.diff.block_hash);
let state_root = non_placeholder_hash(payload.diff.state_root);
let transactions_root = payload
.diff
.transactions_root
.and_then(non_placeholder_hash);
let parent_hash = non_placeholder_hash(active.base.parent_hash);
let prevrandao = active.base.prevrandao.and_then(non_placeholder_hash);
let content_hash = flashblock_content_hash(FlashblockContentCommitment {
provider: &self.provider,
payload_id: Some(payload.payload_id),
index: Some(payload.index),
block_number: active.base.block_number,
partial_block_hash,
parent_hash,
state_root,
transactions_root,
transaction_hashes: &cumulative_transactions,
timestamp: Some(active.base.timestamp),
base_fee_per_gas: active.base.base_fee_per_gas,
beneficiary: active.base.beneficiary,
prevrandao,
gas_limit: active.base.gas_limit,
});
for log in &mut logs {
log.block_hash = Some(content_hash);
}
let flashblock = FlashblockRef {
provider: self.provider.clone(),
payload_id: Some(payload.payload_id),
index: Some(payload.index),
block_number: active.base.block_number,
content_hash,
partial_block_hash,
parent_hash,
state_root,
transactions_root,
transaction_hashes: cumulative_transactions.clone(),
timestamp: Some(active.base.timestamp),
base_fee_per_gas: active.base.base_fee_per_gas,
beneficiary: active.base.beneficiary,
prevrandao,
gas_limit: active.base.gas_limit,
};
let next_log_index = active
.next_log_index
.checked_add(
u64::try_from(logs.len())
.map_err(|_| RawJsonFlashblocksError::ResourceExhausted("log index"))?,
)
.ok_or(RawJsonFlashblocksError::ResourceExhausted("log index"))?;
active.transaction_set.extend(delta_transactions);
active.cumulative_transactions = cumulative_transactions;
active.last_index = Some(payload.index);
active.index_commitments.insert(payload.index, commitment);
active.next_log_index = next_log_index;
active.cumulative_logs = active.cumulative_logs.saturating_add(logs.len());
Ok(Some(FlashblockUpdate::Snapshot(Box::new(
FlashblockSnapshot { flashblock, logs },
))))
}
fn invalidation(
&self,
payload_id: FixedBytes<8>,
reason: FlashblockInvalidationReason,
) -> FlashblockUpdate {
FlashblockUpdate::Invalidated(FlashblockInvalidation {
provider: self.provider.clone(),
payload_id,
reason,
})
}
fn invalidate_active(
&mut self,
payload_id: FixedBytes<8>,
reason: FlashblockInvalidationReason,
) -> FlashblockUpdate {
self.active = None;
self.ignored_payload = Some(payload_id);
self.invalidation(payload_id, reason)
}
}
const MIN_BUFFERED_GAP_MILLIS: u64 = 300;
const MAX_BUFFERED_GAP_MILLIS: u64 = 500;
#[derive(Clone, Debug)]
pub struct BufferedRawJsonFlashblocksAdapter {
inner: RawJsonFlashblocksAdapter,
gap_timeout_millis: u64,
buffered: Option<BufferedRawFlashblock>,
}
#[derive(Clone, Debug)]
struct BufferedRawFlashblock {
payload: RawFlashblockPayload,
commitment: B256,
expected_index: u64,
source_ingress_millis: u64,
expires_at_millis: u64,
}
impl BufferedRawJsonFlashblocksAdapter {
pub fn new(
provider: ProviderRef,
limits: RawJsonFlashblocksLimits,
gap_timeout_millis: u64,
) -> Result<Self, RawJsonFlashblocksError> {
if !(MIN_BUFFERED_GAP_MILLIS..=MAX_BUFFERED_GAP_MILLIS).contains(&gap_timeout_millis) {
return Err(RawJsonFlashblocksError::InvalidLimits(
"buffered gap timeout must be between 300 and 500 milliseconds",
));
}
Ok(Self {
inner: RawJsonFlashblocksAdapter::with_limits(provider, limits)?,
gap_timeout_millis,
buffered: None,
})
}
pub const fn provider(&self) -> &ProviderRef {
self.inner.provider()
}
pub const fn limits(&self) -> RawJsonFlashblocksLimits {
self.inner.limits()
}
pub fn buffered_gap(&self) -> Option<(u64, u64, u64)> {
self.buffered.as_ref().map(|buffered| {
(
buffered.expected_index,
buffered.payload.index,
buffered.expires_at_millis,
)
})
}
pub fn ingest_json_at(
&mut self,
frame: &[u8],
now_millis: u64,
) -> Result<Vec<FlashblockUpdate>, RawJsonFlashblocksError> {
self.ingest_json_timed_at(frame, now_millis).map(|updates| {
updates
.into_iter()
.map(TimedFlashblockUpdate::into_update)
.collect()
})
}
pub fn ingest_json_timed_at(
&mut self,
frame: &[u8],
now_millis: u64,
) -> Result<Vec<TimedFlashblockUpdate>, RawJsonFlashblocksError> {
let payload = self.inner.decode_json(frame)?;
let mut updates = Vec::with_capacity(2);
if self
.buffered
.as_ref()
.is_some_and(|buffered| now_millis >= buffered.expires_at_millis)
{
let expired = self.buffered.as_ref().expect("checked above");
if payload.payload_id == expired.payload.payload_id {
let payload_id = expired.payload.payload_id;
self.buffered = None;
updates.push(TimedFlashblockUpdate::new(
self.inner
.invalidate_active(payload_id, FlashblockInvalidationReason::IndexGap),
now_millis,
));
return Ok(updates);
}
let payload_id = expired.payload.payload_id;
let invalidation = self
.inner
.invalidation(payload_id, FlashblockInvalidationReason::IndexGap);
let mut staged = self.inner.clone();
let replacement = staged.ingest(payload)?;
self.inner = staged;
self.buffered = None;
updates.push(TimedFlashblockUpdate::new(invalidation, now_millis));
if let Some(update) = replacement {
updates.push(TimedFlashblockUpdate::new(update, now_millis));
}
return Ok(updates);
}
if let Some(buffered) = self.buffered.as_ref() {
if payload.payload_id == buffered.payload.payload_id {
if payload.index == buffered.expected_index {
let buffered = self.buffered.as_ref().expect("checked above").clone();
let mut staged = self.inner.clone();
if let Some(update) = staged.ingest(payload)? {
updates.push(TimedFlashblockUpdate::new(update, now_millis));
}
if let Some(update) = staged.ingest(buffered.payload)? {
updates.push(TimedFlashblockUpdate::new(
update,
buffered.source_ingress_millis,
));
}
self.inner = staged;
self.buffered = None;
return Ok(updates);
}
if payload.index == buffered.payload.index {
let commitment = self.validate_bufferable_payload(&payload)?;
if commitment == buffered.commitment {
return Ok(updates);
}
let payload_id = payload.payload_id;
self.buffered = None;
updates.push(TimedFlashblockUpdate::new(
self.inner.invalidate_active(
payload_id,
FlashblockInvalidationReason::ConflictingDuplicate,
),
now_millis,
));
return Ok(updates);
}
if payload.index > buffered.payload.index {
self.validate_bufferable_payload(&payload)?;
let payload_id = payload.payload_id;
self.buffered = None;
updates.push(TimedFlashblockUpdate::new(
self.inner
.invalidate_active(payload_id, FlashblockInvalidationReason::IndexGap),
now_millis,
));
return Ok(updates);
}
} else {
let mut staged = self.inner.clone();
let replacement = staged.ingest(payload)?;
self.inner = staged;
self.buffered = None;
if let Some(update) = replacement {
updates.push(TimedFlashblockUpdate::new(update, now_millis));
}
return Ok(updates);
}
}
if self.can_buffer_one_gap(&payload) {
let commitment = self.validate_bufferable_payload(&payload)?;
let active = self
.inner
.active
.as_ref()
.expect("buffering requires active state");
let expected_index = active.last_index.map_or(0, |index| index.saturating_add(1));
self.buffered = Some(BufferedRawFlashblock {
payload,
commitment,
expected_index,
source_ingress_millis: now_millis,
expires_at_millis: now_millis.saturating_add(self.gap_timeout_millis),
});
return Ok(updates);
}
if self.is_more_than_one_index_ahead(&payload) {
self.validate_bufferable_payload(&payload)?;
}
if let Some(update) = self.inner.ingest(payload)? {
if matches!(update, FlashblockUpdate::Invalidated(_)) {
self.buffered = None;
}
updates.push(TimedFlashblockUpdate::new(update, now_millis));
}
Ok(updates)
}
pub fn expire_gap_at(&mut self, now_millis: u64) -> Option<FlashblockUpdate> {
let expired = self
.buffered
.as_ref()
.is_some_and(|buffered| now_millis >= buffered.expires_at_millis);
if !expired {
return None;
}
let buffered = self.buffered.take().expect("checked above");
Some(self.inner.invalidate_active(
buffered.payload.payload_id,
FlashblockInvalidationReason::IndexGap,
))
}
pub fn reset(
&mut self,
provider: ProviderRef,
) -> Result<Option<FlashblockUpdate>, RawJsonFlashblocksError> {
let invalidation = self.inner.reset(provider)?;
self.buffered = None;
Ok(invalidation)
}
fn can_buffer_one_gap(&self, payload: &RawFlashblockPayload) -> bool {
let Some(active) = self.inner.active.as_ref() else {
return false;
};
if active.payload_id != payload.payload_id {
return false;
}
let expected = active.last_index.map_or(0, |index| index.saturating_add(1));
payload.index == expected.saturating_add(1)
}
fn is_more_than_one_index_ahead(&self, payload: &RawFlashblockPayload) -> bool {
let Some(active) = self.inner.active.as_ref() else {
return false;
};
if active.payload_id != payload.payload_id {
return false;
}
let expected = active.last_index.map_or(0, |index| index.saturating_add(1));
payload.index > expected.saturating_add(1)
}
fn validate_bufferable_payload(
&self,
payload: &RawFlashblockPayload,
) -> Result<B256, RawJsonFlashblocksError> {
let active = self
.inner
.active
.as_ref()
.expect("buffer validation requires active state");
if payload
.metadata
.block_number
.is_some_and(|number| number != active.base.block_number)
|| payload
.base
.as_ref()
.is_some_and(|base| base != &active.base)
{
return Err(RawJsonFlashblocksError::InvalidPayload(
"base header or metadata block numbers disagree with the active payload".into(),
));
}
let transaction_hashes = flashblock_transaction_hashes(&payload.diff.transactions)
.map_err(|error| RawJsonFlashblocksError::InvalidPayload(error.to_string()))?;
let unique_transactions = transaction_hashes.iter().copied().collect::<HashSet<_>>();
if unique_transactions.len() != transaction_hashes.len() {
return Err(RawJsonFlashblocksError::InvalidPayload(
"the transaction delta contains a duplicate hash".into(),
));
}
let receipt_hashes = payload
.metadata
.receipts
.keys()
.copied()
.collect::<HashSet<_>>();
if receipt_hashes != unique_transactions {
return Err(RawJsonFlashblocksError::InvalidPayload(
"receipt-map membership disagrees with the transaction delta".into(),
));
}
if active
.cumulative_transactions
.len()
.saturating_add(transaction_hashes.len())
> self.inner.limits.max_transactions_per_payload
{
return Err(RawJsonFlashblocksError::ResourceExhausted(
"transaction count",
));
}
if transaction_hashes
.iter()
.any(|hash| active.transaction_set.contains(hash))
{
return Err(RawJsonFlashblocksError::InvalidPayload(
"a transaction appeared in more than one indexed delta".into(),
));
}
let log_count =
payload
.metadata
.receipts
.values()
.try_fold(0_usize, |count, receipt| {
for log in &receipt.logs {
if log.topics.len() > 4 {
return Err(RawJsonFlashblocksError::InvalidPayload(
"receipt log contains more than four topics".into(),
));
}
}
count
.checked_add(receipt.logs.len())
.ok_or(RawJsonFlashblocksError::ResourceExhausted("log count"))
})?;
if active.cumulative_logs.saturating_add(log_count) > self.inner.limits.max_logs_per_payload
{
return Err(RawJsonFlashblocksError::ResourceExhausted("log count"));
}
u64::try_from(
active
.cumulative_transactions
.len()
.saturating_add(transaction_hashes.len()),
)
.map_err(|_| RawJsonFlashblocksError::ResourceExhausted("transaction index"))?;
active
.next_log_index
.checked_add(
u64::try_from(log_count)
.map_err(|_| RawJsonFlashblocksError::ResourceExhausted("log index"))?,
)
.ok_or(RawJsonFlashblocksError::ResourceExhausted("log index"))?;
Ok(raw_payload_commitment(payload, &transaction_hashes))
}
}
fn raw_payload_commitment(payload: &RawFlashblockPayload, transaction_hashes: &[B256]) -> B256 {
let mut commitment = Keccak256::new();
commitment.update(b"evm-fork-cache/raw-json-flashblock/v1");
commitment.update(payload.payload_id.as_slice());
commitment.update(payload.index.to_be_bytes());
match payload.base.as_ref() {
Some(base) => {
commitment.update([1]);
commitment.update(base.parent_hash.as_slice());
commitment.update(base.block_number.to_be_bytes());
commitment.update(base.timestamp.to_be_bytes());
commit_optional_raw_u64(&mut commitment, base.gas_limit);
commit_optional_raw_u64(&mut commitment, base.base_fee_per_gas);
commit_optional_raw_bytes(
&mut commitment,
base.beneficiary.as_ref().map(|address| address.as_slice()),
);
commit_optional_raw_bytes(
&mut commitment,
base.prevrandao.as_ref().map(B256::as_slice),
);
}
None => commitment.update([0]),
}
commitment.update(payload.diff.state_root.as_slice());
commitment.update(payload.diff.block_hash.as_slice());
commit_optional_raw_bytes(
&mut commitment,
payload.diff.transactions_root.as_ref().map(B256::as_slice),
);
commit_optional_raw_u64(&mut commitment, payload.metadata.block_number);
commitment.update((transaction_hashes.len() as u64).to_be_bytes());
for transaction_hash in transaction_hashes {
commitment.update(transaction_hash.as_slice());
let receipt = payload
.metadata
.receipts
.get(transaction_hash)
.expect("receipt membership validated before commitment");
commitment.update((receipt.logs.len() as u64).to_be_bytes());
for log in &receipt.logs {
commitment.update(log.address.as_slice());
commitment.update((log.topics.len() as u64).to_be_bytes());
for topic in &log.topics {
commitment.update(topic.as_slice());
}
commitment.update((log.data.len() as u64).to_be_bytes());
commitment.update(log.data.as_ref());
}
}
commitment.finalize()
}
fn commit_optional_raw_u64(commitment: &mut Keccak256, value: Option<u64>) {
match value {
Some(value) => {
commitment.update([1]);
commitment.update(value.to_be_bytes());
}
None => commitment.update([0]),
}
}
fn commit_optional_raw_bytes(commitment: &mut Keccak256, value: Option<&[u8]>) {
match value {
Some(value) => {
commitment.update([1]);
commitment.update((value.len() as u64).to_be_bytes());
commitment.update(value);
}
None => commitment.update([0]),
}
}
#[derive(Clone, Debug)]
struct RawPayloadState {
payload_id: FixedBytes<8>,
base: BaseFlashblockBase,
last_index: Option<u64>,
cumulative_transactions: Vec<B256>,
transaction_set: HashSet<B256>,
next_log_index: u64,
cumulative_logs: usize,
index_commitments: HashMap<u64, B256>,
}
#[derive(Clone, Debug, serde::Deserialize)]
struct RawFlashblockPayload {
payload_id: FixedBytes<8>,
index: u64,
#[serde(default, alias = "static")]
base: Option<BaseFlashblockBase>,
diff: RawFlashblockDiff,
metadata: RawFlashblockMetadata,
}
#[derive(Clone, Debug, serde::Deserialize)]
struct RawFlashblockDiff {
state_root: B256,
block_hash: B256,
#[serde(default)]
transactions: Vec<serde_json::Value>,
#[serde(default)]
transactions_root: Option<B256>,
}
#[derive(Clone, Debug, serde::Deserialize)]
struct RawFlashblockMetadata {
#[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
block_number: Option<u64>,
#[serde(default, deserialize_with = "deserialize_receipts")]
receipts: HashMap<B256, RawTransactionReceipt>,
}
fn deserialize_receipts<'de, D>(
deserializer: D,
) -> Result<HashMap<B256, RawTransactionReceipt>, D::Error>
where
D: serde::Deserializer<'de>,
{
struct ReceiptsVisitor;
impl<'de> serde::de::Visitor<'de> for ReceiptsVisitor {
type Value = HashMap<B256, RawTransactionReceipt>;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a receipt map with unique transaction-hash keys")
}
fn visit_map<A>(self, mut entries: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut receipts = HashMap::with_capacity(entries.size_hint().unwrap_or_default());
while let Some((transaction_hash, receipt)) = entries.next_entry()? {
if receipts.insert(transaction_hash, receipt).is_some() {
return Err(serde::de::Error::custom("duplicate receipt key"));
}
}
Ok(receipts)
}
}
deserializer.deserialize_map(ReceiptsVisitor)
}
#[derive(Clone, Debug, serde::Deserialize)]
struct RawTransactionReceipt {
#[serde(default)]
logs: Vec<RawReceiptLog>,
}
#[derive(Clone, Debug, serde::Deserialize)]
struct RawReceiptLog {
address: Address,
#[serde(default)]
topics: Vec<B256>,
data: Bytes,
}