@@ -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