Skip to content

Commit 28276d5

Browse files
authored
Merge pull request DMDcoin#174 from dforsten/dforsten/random_transactions_order
Front-running Resistance: Implemented a function to deterministically shuffle transactions from validator contributions (DMDcoin#89)
2 parents 34614fd + b91b4ca commit 28276d5

File tree

2 files changed

+119
-0
lines changed

2 files changed

+119
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub mod bound_contract;
2+
mod transactions_shuffling;
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Warning: Part of the Consensus protocol, changes need to produce *exactly* the same result or
2+
// block verification will fail. Intentional breaking changes constitute a fork.
3+
4+
use ethereum_types::{Address, U256};
5+
use std::collections::HashMap;
6+
use types::transaction::SignedTransaction;
7+
8+
/// Combining an address with a random U256 seed using XOR, using big-endian byte ordering always.
9+
fn address_xor_u256(address: &Address, seed: U256) -> Address {
10+
// Address bytes are always assuming big-endian order.
11+
let address_bytes = address.as_bytes();
12+
13+
// Explicitly convert U256 to big endian order
14+
let mut seed_bytes = [0u8; 32];
15+
seed.to_big_endian(&mut seed_bytes);
16+
17+
// Byte-wise XOR, constructing a new, big-endian array
18+
let mut result = [0u8; 20];
19+
for i in 0..20 {
20+
result[i] = address_bytes[i] ^ seed_bytes[i];
21+
}
22+
23+
// Construct a new Address from the big-endian array
24+
Address::from(result)
25+
}
26+
27+
/// The list of transactions is expected to be free of duplicates.
28+
fn deterministic_transactions_shuffling(
29+
transactions: Vec<SignedTransaction>,
30+
seed: U256,
31+
) -> Vec<SignedTransaction> {
32+
// The implementation needs to be both portable and deterministic.
33+
// There is no guarantee that the input list of transactions does not contain transactions
34+
// with the same nonce but different content.
35+
// There is also no guarantee the transactions are sorted by nonce.
36+
37+
// Group transactions by sender.
38+
// * Walk the transactions from first to last
39+
// * Add transactions with unique nonce to a per-sender vector
40+
// * Discard transactions with a nonce already existing in the list of transactions
41+
let mut txs_by_sender: HashMap<_, Vec<SignedTransaction>> = HashMap::new();
42+
for tx in transactions {
43+
let sender = tx.sender();
44+
let entry = txs_by_sender.entry(sender).or_insert_with(Vec::new);
45+
46+
if let Some(existing_tx) = entry
47+
.iter_mut()
48+
.find(|existing_tx| existing_tx.tx().nonce == tx.tx().nonce)
49+
{
50+
if tx.tx().gas_price > existing_tx.tx().gas_price {
51+
*existing_tx = tx;
52+
}
53+
} else {
54+
entry.push(tx);
55+
}
56+
}
57+
58+
// For each sender, sort their transactions by nonce (lowest first).
59+
// Nonces are expected to be unique at this point, guaranteeing portable
60+
// and deterministic results independent of the sorting algorithm as long as
61+
// the sorting algorithm works and is implemented correctly.
62+
for txs in txs_by_sender.values_mut() {
63+
txs.sort_by_key(|tx| tx.tx().nonce);
64+
}
65+
66+
// Deterministically randomize the order of senders.
67+
// Same as with transactions we rely on the uniqueness of list members and
68+
// a properly functioning sorting algorithm. To prevent predictable order we
69+
// XOR each sender address with the random number generated through the HBBFT
70+
// protocol, and use the resulting address as sorting key.
71+
// The random number is guaranteed to be identical for all validators at the
72+
// time of block creation.
73+
let mut senders: Vec<_> = txs_by_sender.keys().cloned().collect();
74+
senders.sort_by_key(|address| address_xor_u256(address, seed));
75+
76+
// Create the final transaction list by iterating over the randomly shuffled senders.
77+
let mut final_transactions = Vec::new();
78+
for sender in senders {
79+
if let Some(mut sender_txs) = txs_by_sender.remove(&sender) {
80+
// Each sender's transactions are already sorted by nonce.
81+
final_transactions.append(&mut sender_txs);
82+
}
83+
}
84+
85+
final_transactions
86+
}
87+
88+
#[cfg(test)]
89+
mod tests {
90+
use super::*;
91+
// Convert to bytes in big-endian order.
92+
93+
fn u64_to_bytes_be<const N: usize>(n: u64) -> [u8; N] {
94+
// Make sure the array is large enough to hold 8 bytes.
95+
assert!(N >= 8, "Target array size must be at least 8 bytes");
96+
let mut result = [0u8; N];
97+
// Copy the big-endian bytes into the first 8 bytes.
98+
result[..8].copy_from_slice(&n.to_be_bytes());
99+
result
100+
}
101+
102+
#[test]
103+
fn test_address_xor_u256() {
104+
// TODO: Cover corner cases, preferably by using a testing crate like proptest.
105+
let address_value = 0x1234567890abcdefu64;
106+
let seed_value = 0x7a9e4b3d1c2f0a68u64;
107+
108+
let address_bytes: [u8; 20] = u64_to_bytes_be(address_value);
109+
let address = Address::from_slice(&address_bytes);
110+
let seed_bytes: [u8; 32] = u64_to_bytes_be(seed_value);
111+
let seed = U256::from_big_endian(&seed_bytes);
112+
let result = address_xor_u256(&address, seed);
113+
assert_eq!(
114+
result,
115+
Address::from_slice(&u64_to_bytes_be::<20>(address_value ^ seed_value))
116+
);
117+
}
118+
}

0 commit comments

Comments
 (0)