Skip to content

Commit e6a0af2

Browse files
refactor(evm): add TransactionKey wrapper (#1331)
* conditional storage compression * add tests * rename CompressedBincode to CompactBincode * add TransactionKey wrapper * tests --------- Co-authored-by: sebastijankuzner <sebastijan.kuzner@outlook.com>
1 parent a15e796 commit e6a0af2

1 file changed

Lines changed: 157 additions & 47 deletions

File tree

  • packages/evm/core/src

packages/evm/core/src/db.rs

Lines changed: 157 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -77,21 +77,60 @@ impl heed::BytesEncode<'_> for HashWrapper {
7777
}
7878
}
7979

80-
#[derive(Debug)]
81-
pub(crate) struct StringWrapper(String);
82-
impl heed::BytesEncode<'_> for StringWrapper {
83-
type EItem = StringWrapper;
80+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
81+
pub struct TransactionKey {
82+
pub block_number: u64,
83+
pub index: u16,
84+
}
85+
86+
impl TransactionKey {
87+
pub fn new(block_number: u64, index: u16) -> Self {
88+
Self {
89+
block_number,
90+
index,
91+
}
92+
}
93+
94+
// Parses the "<block_number>-<index>" token exchanged across the napi boundary.
95+
pub fn parse(token: &str) -> Option<Self> {
96+
let (block_number, index) = token.split_once('-')?;
97+
Some(Self {
98+
block_number: block_number.parse().ok()?,
99+
index: index.parse().ok()?,
100+
})
101+
}
102+
103+
pub fn to_token(&self) -> String {
104+
format!("{}-{}", self.block_number, self.index)
105+
}
106+
}
107+
108+
impl heed::BytesEncode<'_> for TransactionKey {
109+
type EItem = TransactionKey;
84110

85111
fn bytes_encode(item: &Self::EItem) -> Result<Cow<'_, [u8]>, heed::BoxedError> {
86-
Ok(Cow::Borrowed(item.0.as_bytes()))
112+
let mut buffer = Vec::with_capacity(10);
113+
buffer.extend_from_slice(&item.block_number.to_be_bytes());
114+
buffer.extend_from_slice(&item.index.to_be_bytes());
115+
Ok(Cow::Owned(buffer))
87116
}
88117
}
89118

90-
impl heed::BytesDecode<'_> for StringWrapper {
91-
type DItem = StringWrapper;
119+
impl heed::BytesDecode<'_> for TransactionKey {
120+
type DItem = TransactionKey;
92121

93122
fn bytes_decode(bytes: &'_ [u8]) -> Result<Self::DItem, heed::BoxedError> {
94-
Ok(StringWrapper(String::from_utf8(bytes.into())?))
123+
let Some((block_number, rest)) = bytes.split_first_chunk::<8>() else {
124+
return Err("TransactionKey: truncated key".into());
125+
};
126+
let Some((index, _)) = rest.split_first_chunk::<2>() else {
127+
return Err("TransactionKey: truncated key".into());
128+
};
129+
130+
Ok(TransactionKey {
131+
block_number: u64::from_be_bytes(*block_number),
132+
index: u16::from_be_bytes(*index),
133+
})
95134
}
96135
}
97136

@@ -170,8 +209,8 @@ pub(crate) struct InnerStorage {
170209
pub proofs: heed::Database<HeedBlockNumber, CompactBincode<ProofData>>,
171210
pub blocks: heed::Database<HeedBlockNumber, CompactBincode<BlockHeaderData>>,
172211
pub blocks_hash_number: heed::Database<HashWrapper, HeedBlockNumber>,
173-
pub transactions: heed::Database<StringWrapper, CompactBincode<TransactionData>>,
174-
pub transactions_hash_key: heed::Database<HashWrapper, heed::types::SerdeBincode<String>>,
212+
pub transactions: heed::Database<TransactionKey, CompactBincode<TransactionData>>,
213+
pub transactions_hash_key: heed::Database<HashWrapper, TransactionKey>,
175214
//
176215
}
177216

@@ -426,15 +465,14 @@ impl PersistentDB {
426465
&mut wtxn,
427466
Some("blocks_hash_number"),
428467
)?;
429-
let transactions = env.create_database::<StringWrapper, CompactBincode<TransactionData>>(
468+
let transactions = env.create_database::<TransactionKey, CompactBincode<TransactionData>>(
430469
&mut wtxn,
431470
Some("transactions"),
432471
)?;
433-
let transactions_hash_key = env
434-
.create_database::<HashWrapper, heed::types::SerdeBincode<String>>(
435-
&mut wtxn,
436-
Some("transactions_hash_key"),
437-
)?;
472+
let transactions_hash_key = env.create_database::<HashWrapper, TransactionKey>(
473+
&mut wtxn,
474+
Some("transactions_hash_key"),
475+
)?;
438476

439477
wtxn.commit()?;
440478

@@ -1018,7 +1056,9 @@ impl PersistentDB {
10181056

10191057
// Update transactions
10201058
for (sequence, _) in transactions.iter().enumerate() {
1021-
let key = format!("{}-{}", key.0, sequence);
1059+
debug_assert!(sequence <= u16::MAX as usize);
1060+
1061+
let key = TransactionKey::new(key.0, sequence as u16);
10221062
let transaction = &transactions[sequence];
10231063

10241064
inner.transactions_hash_key.put(
@@ -1027,11 +1067,9 @@ impl PersistentDB {
10271067
&key,
10281068
)?;
10291069

1030-
inner.transactions.put(
1031-
rwtxn,
1032-
&StringWrapper(key),
1033-
&CompactBincode(transaction),
1034-
)?;
1070+
inner
1071+
.transactions
1072+
.put(rwtxn, &key, &CompactBincode(transaction))?;
10351073
}
10361074

10371075
// Update state
@@ -1155,15 +1193,19 @@ impl PersistentDB {
11551193
Ok(inner.blocks.get(&rtxn, &block_number)?.map(|data| data.0))
11561194
}
11571195

1158-
pub fn get_transaction_data(&self, key: String) -> Result<Option<TransactionData>, Error> {
1196+
pub fn get_transaction(&self, key: TransactionKey) -> Result<Option<TransactionData>, Error> {
11591197
let env = self.env.clone();
11601198
let rtxn = env.read_txn().expect("read");
11611199
let inner = self.inner.borrow();
11621200

1163-
Ok(inner
1164-
.transactions
1165-
.get(&rtxn, &StringWrapper(key))?
1166-
.map(|data| data.0))
1201+
Ok(inner.transactions.get(&rtxn, &key)?.map(|data| data.0))
1202+
}
1203+
1204+
pub fn get_transaction_data(&self, key: String) -> Result<Option<TransactionData>, Error> {
1205+
match TransactionKey::parse(&key) {
1206+
Some(key) => self.get_transaction(key),
1207+
None => Ok(None),
1208+
}
11671209
}
11681210

11691211
pub fn get_transaction_key_by_hash(&self, tx_hash: B256) -> Result<Option<String>, Error> {
@@ -1173,7 +1215,8 @@ impl PersistentDB {
11731215

11741216
Ok(inner
11751217
.transactions_hash_key
1176-
.get(&rtxn, &HashWrapper(tx_hash))?)
1218+
.get(&rtxn, &HashWrapper(tx_hash))?
1219+
.map(|key| key.to_token()))
11771220
}
11781221
}
11791222

@@ -1246,7 +1289,7 @@ mod tests {
12461289
db::{
12471290
AddressWrapper, BlockHeaderData, CommitData, CommitKey, CommitReceipts, HashWrapper,
12481291
LegacyAddressWrapper, MAP_SIZE_UNIT, PendingCommit, PersistentDB, PersistentDBOptions,
1249-
ProofData, StaticStringWrapper, StorageEntryWrapper, StringWrapper, TransactionData,
1292+
ProofData, StaticStringWrapper, StorageEntryWrapper, TransactionData, TransactionKey,
12501293
next_map_size,
12511294
},
12521295
historical::HistoricalAccountData,
@@ -1852,17 +1895,6 @@ mod tests {
18521895
assert_eq!(legacy_address, deserialized.0);
18531896
}
18541897

1855-
#[test]
1856-
fn test_string_wrapper() {
1857-
let string = "test".to_owned();
1858-
1859-
let wrapper = StringWrapper(string);
1860-
let serialized = <StringWrapper as BytesEncode>::bytes_encode(&wrapper).expect("ok");
1861-
let deserialized = <StringWrapper as BytesDecode>::bytes_decode(&serialized).expect("ok");
1862-
1863-
assert_eq!("test", deserialized.0);
1864-
}
1865-
18661898
#[test]
18671899
fn test_static_string_wrapper() {
18681900
let string = "test";
@@ -2388,12 +2420,87 @@ mod tests {
23882420
);
23892421
}
23902422

2423+
#[test]
2424+
fn test_transaction_key_encode_decode_roundtrip() {
2425+
for (block_number, index) in [(0u64, 0u16), (1, 2), (5, 9999), (u64::MAX, u16::MAX)] {
2426+
let key = TransactionKey::new(block_number, index);
2427+
let encoded = <TransactionKey as BytesEncode>::bytes_encode(&key).unwrap();
2428+
assert_eq!(encoded.len(), 10, "key is 8-byte block + 2-byte index");
2429+
2430+
let decoded = <TransactionKey as BytesDecode>::bytes_decode(&encoded).unwrap();
2431+
assert_eq!(decoded, key);
2432+
}
2433+
}
2434+
2435+
#[test]
2436+
fn test_transaction_key_orders_by_block_then_index() {
2437+
// The transactions DB relies on byte (memcmp) key order matching numeric
2438+
// (block_number, index) order, so get_commits_by_block_range can scan by block number.
2439+
// Big-endian encoding is what guarantees this (e.g. block 2 sorts before block 10).
2440+
let ascending = [
2441+
TransactionKey::new(0, 0),
2442+
TransactionKey::new(0, 1),
2443+
TransactionKey::new(0, u16::MAX),
2444+
TransactionKey::new(1, 0), // block 1 sorts after every transaction of block 0
2445+
TransactionKey::new(2, 0),
2446+
TransactionKey::new(10, 0), // numeric order, not lexicographic on decimal
2447+
TransactionKey::new(u64::MAX, 0),
2448+
TransactionKey::new(u64::MAX, u16::MAX),
2449+
];
2450+
2451+
for window in ascending.windows(2) {
2452+
let lo = <TransactionKey as BytesEncode>::bytes_encode(&window[0]).unwrap();
2453+
let hi = <TransactionKey as BytesEncode>::bytes_encode(&window[1]).unwrap();
2454+
assert!(lo < hi, "encoded keys must sort by (block, index)");
2455+
// The derived Ord must agree with the on-disk byte order.
2456+
assert!(window[0] < window[1]);
2457+
}
2458+
}
2459+
2460+
#[test]
2461+
fn test_transaction_key_token_roundtrip_and_lenient_parse() {
2462+
assert_eq!(TransactionKey::new(5, 2).to_token(), "5-2");
2463+
2464+
let key = TransactionKey::new(123, 45);
2465+
assert_eq!(TransactionKey::parse(&key.to_token()), Some(key));
2466+
2467+
// Malformed or out-of-range tokens parse to None (treated as "no such transaction").
2468+
assert_eq!(TransactionKey::parse("nope"), None);
2469+
assert_eq!(TransactionKey::parse("-5"), None);
2470+
assert_eq!(TransactionKey::parse("1-2-3"), None);
2471+
assert_eq!(TransactionKey::parse("1-70000"), None); // index exceeds u16::MAX
2472+
}
2473+
2474+
#[test]
2475+
fn test_transaction_key_range_bounds_capture_a_block_range() {
2476+
// Mirrors the scan bounds get_commits_by_block_range builds for [from, to].
2477+
let from = TransactionKey::new(5, 0);
2478+
let to = TransactionKey::new(7, u16::MAX);
2479+
2480+
for block in 5..=7u64 {
2481+
for index in [0u16, 1, 1000, u16::MAX] {
2482+
let key = TransactionKey::new(block, index);
2483+
assert!(
2484+
key >= from && key <= to,
2485+
"{block}-{index} should be within range"
2486+
);
2487+
}
2488+
}
2489+
2490+
// The neighbouring blocks fall outside the range on each side.
2491+
assert!(TransactionKey::new(4, u16::MAX) < from);
2492+
assert!(TransactionKey::new(8, 0) > to);
2493+
}
2494+
23912495
#[test]
23922496
fn test_get_transaction_data() {
23932497
let db = create_temp_database();
23942498

2395-
let key = String::from("tx-1");
2396-
assert_eq!(db.get_transaction_data(key.clone()).unwrap(), None);
2499+
// Lookups go through the "<block>-<index>" token; before anything is written it is absent,
2500+
// and malformed/out-of-range tokens resolve to None rather than erroring.
2501+
assert_eq!(db.get_transaction_data("1-0".into()).unwrap(), None);
2502+
assert_eq!(db.get_transaction_data("not-a-key".into()).unwrap(), None);
2503+
assert_eq!(db.get_transaction_data("1-70000".into()).unwrap(), None);
23972504

23982505
let hash = b256!("0000000000000000000000000000000000000000000000000000000000000001");
23992506

@@ -2405,7 +2512,7 @@ mod tests {
24052512
.transactions
24062513
.put(
24072514
&mut wtxn,
2408-
&StringWrapper(key.clone()),
2515+
&TransactionKey::new(1, 0),
24092516
&CompactBincode(&TransactionData {
24102517
tx_hash: hash,
24112518
..Default::default()
@@ -2417,7 +2524,7 @@ mod tests {
24172524
}
24182525

24192526
assert_eq!(
2420-
db.get_transaction_data(key.clone()).unwrap(),
2527+
db.get_transaction_data("1-0".into()).unwrap(),
24212528
Some(TransactionData {
24222529
tx_hash: hash,
24232530
..Default::default()
@@ -2429,7 +2536,6 @@ mod tests {
24292536
fn test_get_transaction_hash_by_hash() {
24302537
let db = create_temp_database();
24312538

2432-
let key = String::from("tx-1");
24332539
let hash = b256!("0000000000000000000000000000000000000000000000000000000000000001");
24342540

24352541
assert_eq!(db.get_transaction_key_by_hash(hash).unwrap(), None);
@@ -2440,13 +2546,17 @@ mod tests {
24402546
db.inner
24412547
.borrow_mut()
24422548
.transactions_hash_key
2443-
.put(&mut wtxn, &HashWrapper(hash), &key)
2549+
.put(&mut wtxn, &HashWrapper(hash), &TransactionKey::new(1, 0))
24442550
.unwrap();
24452551

24462552
wtxn.commit().unwrap();
24472553
}
24482554

2449-
assert_eq!(db.get_transaction_key_by_hash(hash).unwrap(), Some(key));
2555+
// The stored typed key is returned to the napi boundary as its "<block>-<index>" token.
2556+
assert_eq!(
2557+
db.get_transaction_key_by_hash(hash).unwrap(),
2558+
Some("1-0".to_string())
2559+
);
24502560
}
24512561

24522562
#[test]

0 commit comments

Comments
 (0)