Skip to content
This repository was archived by the owner on Jan 12, 2026. It is now read-only.

Commit a835405

Browse files
committed
feat: implement Block-STM parallel block building
1 parent ac1f545 commit a835405

26 files changed

Lines changed: 5878 additions & 51 deletions

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ repository = "https://github.com/flashbots/op-rbuilder"
88
exclude = [".github/"]
99

1010
[workspace]
11-
members = [ "crates/op-rbuilder", "crates/p2p", "crates/tdx-quote-provider"]
11+
members = ["crates/op-rbuilder", "crates/p2p", "crates/tdx-quote-provider"]
1212
default-members = ["crates/op-rbuilder"]
1313
resolver = "2"
1414

@@ -216,4 +216,4 @@ opentelemetry = { version = "0.31", features = ["trace"] }
216216

217217
# Base Path
218218
concurrent-queue = "2.5.0"
219-
tips-core = { git = "https://github.com/base/tips", rev = "c08eaa4fe10c26de8911609b41ddab4918698325", default-features = false }
219+
tips-core = { git = "https://github.com/base/tips", rev = "c08eaa4fe10c26de8911609b41ddab4918698325", default-features = false }

crates/op-rbuilder/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ op-alloy-flz.workspace = true
9191
revm.workspace = true
9292
op-revm.workspace = true
9393

94+
auto_impl.workspace = true
9495
tracing.workspace = true
9596
eyre.workspace = true
9697
serde_with.workspace = true

crates/op-rbuilder/src/args/op.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ pub struct OpRbuilderArgs {
5656
#[arg(long = "builder.tx-data-store-buffer-size", default_value = "10000")]
5757
pub tx_data_store_buffer_size: usize,
5858

59+
/// Number of parallel threads for transaction execution.
60+
/// Defaults to the number of available CPU cores.
61+
#[arg(long = "builder.parallel-threads", env = "BUILDER_PARALLEL_THREADS")]
62+
pub parallel_threads: Option<usize>,
63+
5964
/// Path to builder playgorund to automatically start up the node connected to it
6065
#[arg(
6166
long = "builder.playground",
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
//! Captured Reads - Read Set Tracking for Block-STM
2+
//!
3+
//! During transaction execution, we need to track all state reads to:
4+
//! 1. Detect conflicts when validating
5+
//! 2. Enable push-based invalidation when a dependency is aborted
6+
//!
7+
//! The `CapturedReads` struct records all reads performed during execution,
8+
//! including the version (which transaction wrote the value) for validation.
9+
10+
use crate::block_stm::types::{EvmStateKey, EvmStateValue, ResolvedBalance, Version};
11+
use alloy_primitives::{Address, U256};
12+
use std::collections::HashMap;
13+
14+
/// A single captured read operation.
15+
#[derive(Debug, Clone)]
16+
pub struct CapturedRead {
17+
/// The version from which the value was read.
18+
/// None means the value was read from base state (not from any transaction).
19+
pub version: Option<Version>,
20+
/// The value that was observed.
21+
pub value: EvmStateValue,
22+
}
23+
24+
impl CapturedRead {
25+
/// Create a new captured read from a transaction's write.
26+
pub fn from_version(version: Version, value: EvmStateValue) -> Self {
27+
Self {
28+
version: Some(version),
29+
value,
30+
}
31+
}
32+
33+
/// Create a new captured read from base state.
34+
pub fn from_base_state(value: EvmStateValue) -> Self {
35+
Self {
36+
version: None,
37+
value,
38+
}
39+
}
40+
}
41+
42+
/// A captured resolved balance read (with deltas applied).
43+
#[derive(Debug, Clone)]
44+
pub struct CapturedResolvedBalance {
45+
/// The address whose balance was resolved
46+
pub address: Address,
47+
/// The base value before deltas
48+
pub base_value: U256,
49+
/// The version of the base value (None if from storage)
50+
pub base_version: Option<Version>,
51+
/// The total delta that was applied
52+
pub total_delta: U256,
53+
/// The final resolved value
54+
pub resolved_value: U256,
55+
/// All versions that contributed deltas
56+
pub contributors: Vec<Version>,
57+
}
58+
59+
impl CapturedResolvedBalance {
60+
/// Create from a ResolvedBalance.
61+
pub fn from_resolved(address: Address, resolved: ResolvedBalance) -> Self {
62+
Self {
63+
address,
64+
base_value: resolved.base_value,
65+
base_version: resolved.base_version,
66+
total_delta: resolved.total_delta,
67+
resolved_value: resolved.resolved_value,
68+
contributors: resolved.contributors,
69+
}
70+
}
71+
}
72+
73+
/// Tracks all reads performed during a transaction's execution.
74+
///
75+
/// Used for:
76+
/// - Validation: checking if any reads have become stale
77+
/// - Dependency tracking: knowing which transactions this one depends on
78+
#[derive(Debug, Default)]
79+
pub struct CapturedReads {
80+
/// Map from state key to the read that was performed.
81+
reads: HashMap<EvmStateKey, CapturedRead>,
82+
/// Resolved balance reads (balance reads that included deltas).
83+
/// These are tracked separately because they depend on multiple transactions.
84+
resolved_balances: HashMap<Address, CapturedResolvedBalance>,
85+
}
86+
87+
impl CapturedReads {
88+
/// Create a new empty CapturedReads.
89+
pub fn new() -> Self {
90+
Self {
91+
reads: HashMap::new(),
92+
resolved_balances: HashMap::new(),
93+
}
94+
}
95+
96+
/// Record a read from a transaction's write.
97+
pub fn capture_read(&mut self, key: EvmStateKey, version: Version, value: EvmStateValue) {
98+
self.reads
99+
.insert(key, CapturedRead::from_version(version, value));
100+
}
101+
102+
/// Record a read from base state.
103+
pub fn capture_base_read(&mut self, key: EvmStateKey, value: EvmStateValue) {
104+
self.reads.insert(key, CapturedRead::from_base_state(value));
105+
}
106+
107+
/// Record a resolved balance read (balance with deltas applied).
108+
pub fn capture_resolved_balance(&mut self, address: Address, resolved: ResolvedBalance) {
109+
self.resolved_balances.insert(
110+
address,
111+
CapturedResolvedBalance::from_resolved(address, resolved),
112+
);
113+
}
114+
115+
/// Get all captured reads.
116+
pub fn reads(&self) -> &HashMap<EvmStateKey, CapturedRead> {
117+
&self.reads
118+
}
119+
120+
/// Get all captured resolved balances.
121+
pub fn resolved_balances(&self) -> &HashMap<Address, CapturedResolvedBalance> {
122+
&self.resolved_balances
123+
}
124+
125+
/// Get the set of transaction indices that this transaction depends on.
126+
/// Includes dependencies from both regular reads and resolved balance reads.
127+
pub fn dependencies(&self) -> impl Iterator<Item = u32> + '_ {
128+
// Dependencies from regular reads
129+
let read_deps = self
130+
.reads
131+
.values()
132+
.filter_map(|read| read.version.map(|v| v.txn_idx));
133+
134+
// Dependencies from resolved balances (all contributors)
135+
let balance_deps = self.resolved_balances.values().flat_map(|rb| {
136+
rb.base_version
137+
.iter()
138+
.map(|v| v.txn_idx)
139+
.chain(rb.contributors.iter().map(|v| v.txn_idx))
140+
});
141+
142+
read_deps.chain(balance_deps)
143+
}
144+
145+
/// Check if any read depends on the given transaction index.
146+
pub fn depends_on(&self, txn_idx: u32) -> bool {
147+
// Check regular reads
148+
let has_read_dep = self
149+
.reads
150+
.values()
151+
.any(|read| read.version.map(|v| v.txn_idx) == Some(txn_idx));
152+
153+
if has_read_dep {
154+
return true;
155+
}
156+
157+
// Check resolved balances (base version + contributors)
158+
self.resolved_balances.values().any(|rb| {
159+
rb.base_version.map(|v| v.txn_idx) == Some(txn_idx)
160+
|| rb.contributors.iter().any(|v| v.txn_idx == txn_idx)
161+
})
162+
}
163+
164+
/// Clear all captured reads (for re-execution).
165+
pub fn clear(&mut self) {
166+
self.reads.clear();
167+
self.resolved_balances.clear();
168+
}
169+
170+
/// Get the number of reads captured (regular reads + resolved balances).
171+
pub fn len(&self) -> usize {
172+
self.reads.len() + self.resolved_balances.len()
173+
}
174+
175+
/// Check if no reads have been captured.
176+
pub fn is_empty(&self) -> bool {
177+
self.reads.is_empty() && self.resolved_balances.is_empty()
178+
}
179+
180+
/// Get the original balance for an address (if it was read).
181+
/// Returns None if the balance was never read.
182+
pub fn get_balance(&self, address: Address) -> Option<U256> {
183+
let key = EvmStateKey::Balance(address);
184+
self.reads.get(&key).and_then(|read| {
185+
if let EvmStateValue::Balance(balance) = read.value {
186+
Some(balance)
187+
} else {
188+
None
189+
}
190+
})
191+
}
192+
193+
/// Get the original nonce for an address (if it was read).
194+
/// Returns None if the nonce was never read.
195+
pub fn get_nonce(&self, address: Address) -> Option<u64> {
196+
let key = EvmStateKey::Nonce(address);
197+
self.reads.get(&key).and_then(|read| {
198+
if let EvmStateValue::Nonce(nonce) = read.value {
199+
Some(nonce)
200+
} else {
201+
None
202+
}
203+
})
204+
}
205+
206+
/// Get the original code hash for an address (if it was read).
207+
/// Returns None if the code hash was never read.
208+
pub fn get_code_hash(&self, address: Address) -> Option<alloy_primitives::B256> {
209+
let key = EvmStateKey::CodeHash(address);
210+
self.reads.get(&key).and_then(|read| {
211+
if let EvmStateValue::CodeHash(hash) = read.value {
212+
Some(hash)
213+
} else {
214+
None
215+
}
216+
})
217+
}
218+
}
219+
220+
/// Result of validating a transaction's read set.
221+
#[derive(Debug, Clone, PartialEq, Eq)]
222+
pub enum ValidationResult {
223+
/// All reads are still valid.
224+
Valid,
225+
/// A read has become invalid due to a conflicting write.
226+
Invalid {
227+
/// The key that has a conflict.
228+
key: EvmStateKey,
229+
/// The version we originally read from.
230+
original_version: Option<Version>,
231+
/// The new version that invalidates our read.
232+
new_version: Option<Version>,
233+
},
234+
/// A read from an aborted transaction was detected.
235+
ReadFromAborted {
236+
/// The key that was read from an aborted transaction.
237+
key: EvmStateKey,
238+
/// The aborted transaction index.
239+
aborted_txn_idx: u32,
240+
},
241+
}
242+
243+
impl ValidationResult {
244+
/// Returns true if the validation passed.
245+
pub fn is_valid(&self) -> bool {
246+
matches!(self, ValidationResult::Valid)
247+
}
248+
}
249+
250+
#[cfg(test)]
251+
mod tests {
252+
use super::*;
253+
use alloy_primitives::{Address, U256};
254+
255+
fn test_key(slot: u64) -> EvmStateKey {
256+
EvmStateKey::Storage(Address::ZERO, U256::from(slot))
257+
}
258+
259+
fn test_value(v: u64) -> EvmStateValue {
260+
EvmStateValue::Storage(U256::from(v))
261+
}
262+
263+
#[test]
264+
fn test_capture_read() {
265+
let mut reads = CapturedReads::new();
266+
let key = test_key(1);
267+
let version = Version::new(0, 0);
268+
let value = test_value(42);
269+
270+
reads.capture_read(key.clone(), version, value.clone());
271+
272+
assert_eq!(reads.len(), 1);
273+
let captured = reads.reads().get(&key).unwrap();
274+
assert_eq!(captured.version, Some(version));
275+
assert_eq!(captured.value, value);
276+
}
277+
278+
#[test]
279+
fn test_capture_base_read() {
280+
let mut reads = CapturedReads::new();
281+
let key = test_key(1);
282+
let value = test_value(42);
283+
284+
reads.capture_base_read(key.clone(), value.clone());
285+
286+
let captured = reads.reads().get(&key).unwrap();
287+
assert_eq!(captured.version, None);
288+
assert_eq!(captured.value, value);
289+
}
290+
291+
#[test]
292+
fn test_dependencies() {
293+
let mut reads = CapturedReads::new();
294+
295+
// Read from tx0
296+
reads.capture_read(test_key(1), Version::new(0, 0), test_value(100));
297+
// Read from tx2
298+
reads.capture_read(test_key(2), Version::new(2, 0), test_value(200));
299+
// Read from base state
300+
reads.capture_base_read(test_key(3), test_value(300));
301+
302+
let deps: Vec<_> = reads.dependencies().collect();
303+
assert_eq!(deps.len(), 2);
304+
assert!(deps.contains(&0));
305+
assert!(deps.contains(&2));
306+
}
307+
308+
#[test]
309+
fn test_depends_on() {
310+
let mut reads = CapturedReads::new();
311+
reads.capture_read(test_key(1), Version::new(0, 0), test_value(100));
312+
313+
assert!(reads.depends_on(0));
314+
assert!(!reads.depends_on(1));
315+
assert!(!reads.depends_on(2));
316+
}
317+
318+
#[test]
319+
fn test_clear() {
320+
let mut reads = CapturedReads::new();
321+
reads.capture_read(test_key(1), Version::new(0, 0), test_value(100));
322+
reads.capture_base_read(test_key(2), test_value(200));
323+
324+
assert_eq!(reads.len(), 2);
325+
326+
reads.clear();
327+
328+
assert!(reads.is_empty());
329+
}
330+
}

0 commit comments

Comments
 (0)