Skip to content

Commit 7cc1059

Browse files
authored
Merge pull request #200 from decipherhub/feat/fair-block-ordering
feat: add XOR-based fair block ordering for MEV mitigation
2 parents 842aa50 + 56cb84f commit 7cc1059

File tree

3 files changed

+196
-25
lines changed

3 files changed

+196
-25
lines changed

crates/data-chain/src/cut.rs

Lines changed: 172 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::attestation::AggregatedAttestation;
77
use crate::car::Car;
88
use alloy_primitives::Address;
99
use cipherbft_crypto::BlsPublicKey;
10-
use cipherbft_types::{Hash, ValidatorId};
10+
use cipherbft_types::{Hash, ValidatorId, VALIDATOR_ID_SIZE};
1111
use serde::{Deserialize, Serialize};
1212
use std::collections::HashMap;
1313

@@ -95,8 +95,8 @@ impl Cut {
9595
// Number of Cars
9696
data.extend_from_slice(&(self.cars.len() as u32).to_be_bytes());
9797

98-
// Cars in deterministic order (ValidatorId ascending)
99-
for (_, car) in self.ordered_cars() {
98+
// Cars in deterministic order (ValidatorId ascending for hash stability)
99+
for (_, car) in self.ordered_cars(None) {
100100
// Include Car hash and position
101101
data.extend_from_slice(car.hash().as_bytes());
102102
data.extend_from_slice(&car.position.to_be_bytes());
@@ -120,19 +120,44 @@ impl Cut {
120120
self.attestations.insert(car_hash, attestation);
121121
}
122122

123-
/// Iterate Cars in deterministic order (ValidatorId ascending)
123+
/// Iterate Cars in deterministic order.
124124
///
125-
/// This ensures all validators process transactions in the same order
126-
/// for deterministic deduplication.
127-
pub fn ordered_cars(&self) -> impl Iterator<Item = (&ValidatorId, &Car)> {
125+
/// # Arguments
126+
/// * `parent_hash` - If `Some`, uses XOR-based fair ordering for execution.
127+
/// If `None`, uses ValidatorId ascending order for hashing.
128+
///
129+
/// # Fair Ordering
130+
/// When `parent_hash` is provided, the sort key is:
131+
/// `validator_id[0..20] XOR parent_hash[0..20]`
132+
///
133+
/// This ensures:
134+
/// - Deterministic ordering (all validators compute same order)
135+
/// - Unpredictable ordering (depends on previous block)
136+
/// - Fair rotation (no validator consistently first)
137+
pub fn ordered_cars(
138+
&self,
139+
parent_hash: Option<&Hash>,
140+
) -> impl Iterator<Item = (&ValidatorId, &Car)> {
128141
let mut entries: Vec<_> = self.cars.iter().collect();
129-
entries.sort_by_key(|(vid, _)| *vid);
130-
entries.into_iter()
131-
}
132142

133-
/// Get Cars as ordered Vec (for serialization)
134-
pub fn ordered_cars_vec(&self) -> Vec<(&ValidatorId, &Car)> {
135-
self.ordered_cars().collect()
143+
match parent_hash {
144+
Some(hash) => {
145+
let hash_bytes = hash.as_bytes();
146+
entries.sort_by_key(|(vid, _)| {
147+
let vid_bytes = vid.as_bytes();
148+
let mut sort_key = [0u8; VALIDATOR_ID_SIZE];
149+
for i in 0..VALIDATOR_ID_SIZE {
150+
sort_key[i] = vid_bytes[i] ^ hash_bytes[i];
151+
}
152+
sort_key
153+
});
154+
}
155+
None => {
156+
entries.sort_by_key(|(vid, _)| *vid);
157+
}
158+
}
159+
160+
entries.into_iter()
136161
}
137162

138163
/// Total transaction count across all Cars
@@ -274,7 +299,7 @@ impl Cut {
274299
proposer_address: self.proposer_address,
275300
};
276301

277-
let car_parts = self.ordered_cars().filter_map(move |(_, car)| {
302+
let car_parts = self.ordered_cars(None).filter_map(move |(_, car)| {
278303
let car_hash = car.hash();
279304
self.attestations
280305
.get(&car_hash)
@@ -577,8 +602,8 @@ mod tests {
577602
cut.cars.insert(car2.proposer, car2.clone());
578603
cut.cars.insert(car3.proposer, car3.clone());
579604

580-
// ordered_cars should return in ValidatorId order
581-
let ordered: Vec<_> = cut.ordered_cars().collect();
605+
// ordered_cars(None) should return in ValidatorId order
606+
let ordered: Vec<_> = cut.ordered_cars(None).collect();
582607
assert_eq!(ordered.len(), 3);
583608

584609
// Verify ordering
@@ -1006,4 +1031,135 @@ mod tests {
10061031
}
10071032
panic!("cut was not assembled");
10081033
}
1034+
1035+
// ============ Fair Ordering Tests ============
1036+
1037+
#[test]
1038+
fn test_ordered_cars_fair_differs_from_id_ordering() {
1039+
// Create validators with varied byte patterns to ensure XOR produces different order
1040+
// v1: starts with 0x10, v2: starts with 0x20, v3: starts with 0x30
1041+
let mut v1_bytes = [0u8; VALIDATOR_ID_SIZE];
1042+
let mut v2_bytes = [0u8; VALIDATOR_ID_SIZE];
1043+
let mut v3_bytes = [0u8; VALIDATOR_ID_SIZE];
1044+
1045+
v1_bytes[0] = 0x10;
1046+
v1_bytes[1] = 0xaa;
1047+
v2_bytes[0] = 0x20;
1048+
v2_bytes[1] = 0xbb;
1049+
v3_bytes[0] = 0x30;
1050+
v3_bytes[1] = 0xcc;
1051+
1052+
let v1 = ValidatorId::from_bytes(v1_bytes);
1053+
let v2 = ValidatorId::from_bytes(v2_bytes);
1054+
let v3 = ValidatorId::from_bytes(v3_bytes);
1055+
1056+
let mut cut = Cut::new(1);
1057+
cut.cars.insert(v1, Car::new(v1, 0, vec![], None));
1058+
cut.cars.insert(v2, Car::new(v2, 0, vec![], None));
1059+
cut.cars.insert(v3, Car::new(v3, 0, vec![], None));
1060+
1061+
// With None, should be ValidatorId order: v1 < v2 < v3
1062+
let ordered_none: Vec<_> = cut.ordered_cars(None).map(|(vid, _)| *vid).collect();
1063+
assert_eq!(ordered_none, vec![v1, v2, v3]);
1064+
1065+
// Use a hash that will shuffle the order: 0x25 XOR 0x10=0x35, 0x25 XOR 0x20=0x05, 0x25 XOR 0x30=0x15
1066+
// Expected order after XOR: v2 (0x05) < v3 (0x15) < v1 (0x35)
1067+
let mut hash_bytes = [0u8; 32];
1068+
hash_bytes[0] = 0x25;
1069+
hash_bytes[1] = 0x00;
1070+
let parent_hash = Hash::from_bytes(hash_bytes);
1071+
1072+
let ordered_fair: Vec<_> = cut
1073+
.ordered_cars(Some(&parent_hash))
1074+
.map(|(vid, _)| *vid)
1075+
.collect();
1076+
1077+
// v2 XOR 0x25 = 0x05, v3 XOR 0x25 = 0x15, v1 XOR 0x25 = 0x35
1078+
// So order should be: v2, v3, v1
1079+
assert_eq!(
1080+
ordered_fair,
1081+
vec![v2, v3, v1],
1082+
"XOR should reorder validators"
1083+
);
1084+
}
1085+
1086+
#[test]
1087+
fn test_ordered_cars_fair_is_deterministic() {
1088+
let v1 = ValidatorId::from_bytes([0x10; VALIDATOR_ID_SIZE]);
1089+
let v2 = ValidatorId::from_bytes([0x20; VALIDATOR_ID_SIZE]);
1090+
1091+
let mut cut = Cut::new(1);
1092+
cut.cars.insert(v1, Car::new(v1, 0, vec![], None));
1093+
cut.cars.insert(v2, Car::new(v2, 0, vec![], None));
1094+
1095+
let parent_hash = Hash::compute(b"deterministic_test");
1096+
1097+
// Same inputs should produce same order
1098+
let order1: Vec<_> = cut
1099+
.ordered_cars(Some(&parent_hash))
1100+
.map(|(vid, _)| *vid)
1101+
.collect();
1102+
let order2: Vec<_> = cut
1103+
.ordered_cars(Some(&parent_hash))
1104+
.map(|(vid, _)| *vid)
1105+
.collect();
1106+
1107+
assert_eq!(order1, order2, "Same parent_hash must produce same order");
1108+
}
1109+
1110+
#[test]
1111+
fn test_ordered_cars_fair_rotation() {
1112+
// Different parent hashes should produce different orderings
1113+
let v1 = ValidatorId::from_bytes([0xaa; VALIDATOR_ID_SIZE]);
1114+
let v2 = ValidatorId::from_bytes([0xbb; VALIDATOR_ID_SIZE]);
1115+
let v3 = ValidatorId::from_bytes([0xcc; VALIDATOR_ID_SIZE]);
1116+
1117+
let mut cut = Cut::new(1);
1118+
cut.cars.insert(v1, Car::new(v1, 0, vec![], None));
1119+
cut.cars.insert(v2, Car::new(v2, 0, vec![], None));
1120+
cut.cars.insert(v3, Car::new(v3, 0, vec![], None));
1121+
1122+
let hash1 = Hash::compute(b"block_1");
1123+
let hash2 = Hash::compute(b"block_2");
1124+
let hash3 = Hash::compute(b"block_3");
1125+
1126+
let order1: Vec<_> = cut
1127+
.ordered_cars(Some(&hash1))
1128+
.map(|(vid, _)| *vid)
1129+
.collect();
1130+
let order2: Vec<_> = cut
1131+
.ordered_cars(Some(&hash2))
1132+
.map(|(vid, _)| *vid)
1133+
.collect();
1134+
let order3: Vec<_> = cut
1135+
.ordered_cars(Some(&hash3))
1136+
.map(|(vid, _)| *vid)
1137+
.collect();
1138+
1139+
// At least one ordering should differ (very high probability)
1140+
let all_same = order1 == order2 && order2 == order3;
1141+
assert!(
1142+
!all_same,
1143+
"Different parent hashes should produce different orderings"
1144+
);
1145+
}
1146+
1147+
#[test]
1148+
fn test_ordered_cars_none_preserves_backward_compat() {
1149+
// Regression test: ordered_cars(None) must match original behavior
1150+
let v_low = ValidatorId::from_bytes([0x00; VALIDATOR_ID_SIZE]);
1151+
let v_mid = ValidatorId::from_bytes([0x80; VALIDATOR_ID_SIZE]);
1152+
let v_high = ValidatorId::from_bytes([0xff; VALIDATOR_ID_SIZE]);
1153+
1154+
let mut cut = Cut::new(1);
1155+
// Insert in random order
1156+
cut.cars.insert(v_high, Car::new(v_high, 0, vec![], None));
1157+
cut.cars.insert(v_low, Car::new(v_low, 0, vec![], None));
1158+
cut.cars.insert(v_mid, Car::new(v_mid, 0, vec![], None));
1159+
1160+
let ordered: Vec<_> = cut.ordered_cars(None).map(|(vid, _)| *vid).collect();
1161+
1162+
// Must be ascending ValidatorId order
1163+
assert_eq!(ordered, vec![v_low, v_mid, v_high]);
1164+
}
10091165
}

crates/execution/src/bridge.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
//! # Determinism
1515
//!
1616
//! The bridge guarantees deterministic transaction ordering:
17-
//! - Cars are iterated in ValidatorId ascending order (via `Cut::ordered_cars()`)
17+
//! - Cars are iterated using XOR-based fair ordering (via `Cut::ordered_cars(Some(parent_hash))`)
1818
//! - Within each Car, batch digests are processed in order
1919
//! - Within each Batch, transactions are processed in order
2020
//!
21-
//! This ensures all validators produce identical BlockInputs from the same Cut.
21+
//! This ensures all validators produce identical BlockInputs from the same Cut,
22+
//! while fairly rotating which validator's transactions execute first.
2223
2324
use crate::error::ExecutionError;
2425
use crate::types::BlockInput;
@@ -100,7 +101,7 @@ impl<S: BatchFetcher> ExecutionBridge<S> {
100101
/// # Determinism
101102
///
102103
/// The transaction order is guaranteed to be deterministic:
103-
/// 1. Cars are processed in ValidatorId ascending order
104+
/// 1. Cars are processed using XOR-based fair ordering (parent_hash shuffles order)
104105
/// 2. Batch digests within each Car are processed in order
105106
/// 3. Transactions within each Batch are processed in order
106107
pub async fn convert_cut(
@@ -111,8 +112,11 @@ impl<S: BatchFetcher> ExecutionBridge<S> {
111112
) -> Result<BlockInput> {
112113
let mut all_transactions = Vec::new();
113114

114-
// Iterate Cars in deterministic order (by ValidatorId ascending)
115-
for (_, car) in cut.ordered_cars() {
115+
// Convert B256 to Hash for fair ordering
116+
let parent_hash_typed = Hash::from_bytes(parent_hash.0);
117+
118+
// Iterate Cars in fair order (XOR-based shuffling by parent hash)
119+
for (_, car) in cut.ordered_cars(Some(&parent_hash_typed)) {
116120
for batch_digest in &car.batch_digests {
117121
// Fetch batch from storage
118122
let batch = self
@@ -170,8 +174,11 @@ impl<S: BatchFetcher> ExecutionBridge<S> {
170174
) -> Result<BlockInput> {
171175
let mut all_transactions = Vec::new();
172176

173-
// Iterate Cars in deterministic order (by ValidatorId ascending)
174-
for (_, car) in cut.ordered_cars() {
177+
// Convert B256 to Hash for fair ordering
178+
let parent_hash_typed = Hash::from_bytes(parent_hash.0);
179+
180+
// Iterate Cars in fair order (XOR-based shuffling by parent hash)
181+
for (_, car) in cut.ordered_cars(Some(&parent_hash_typed)) {
175182
for batch_digest in &car.batch_digests {
176183
// Fetch batch from storage
177184
let batch = self

crates/node/src/execution_bridge.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ impl ExecutionBridge {
449449
///
450450
/// This converts the data-chain Cut format to the execution layer format.
451451
/// Fetches actual batches from storage to extract transactions.
452-
/// Uses the tracked `last_block_hash` as the parent hash to maintain chain connectivity.
452+
/// Uses the tracked `last_block_hash` as the parent hash for fair ordering.
453453
async fn convert_cut(
454454
&self,
455455
consensus_cut: cipherbft_data_chain::Cut,
@@ -462,7 +462,15 @@ impl ExecutionBridge {
462462
let mut batches_found = 0usize;
463463
let mut total_txs = 0usize;
464464

465-
for (validator_id, car) in consensus_cut.ordered_cars() {
465+
// Get parent hash for fair ordering (XOR-based shuffling)
466+
let parent_hash_b256 = self
467+
.last_block_hash
468+
.read()
469+
.map(|guard| *guard)
470+
.unwrap_or(B256::ZERO);
471+
let parent_hash = cipherbft_types::Hash::from_bytes(parent_hash_b256.0);
472+
473+
for (validator_id, car) in consensus_cut.ordered_cars(Some(&parent_hash)) {
466474
// Extract transactions from batches by fetching from storage
467475
let mut transactions = Vec::new();
468476
for batch_digest in &car.batch_digests {

0 commit comments

Comments
 (0)