@@ -12,6 +12,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
1212use std:: sync:: { Arc , Mutex } ;
1313
1414use bitcoin:: hashes:: { sha256, Hash , HashEngine } ;
15+ use bitcoin:: hex:: { DisplayHex , FromHex } ;
1516use lightning:: util:: persist:: {
1617 KVStore , PageToken , PaginatedKVStore , PaginatedListResponse , NETWORK_GRAPH_PERSISTENCE_KEY ,
1718 NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE , SCORER_PERSISTENCE_KEY ,
@@ -27,6 +28,7 @@ use crate::logger::{LdkLogger, Logger};
2728use crate :: types:: { DynStore , DynStoreWrapper } ;
2829
2930const INDEX_DATABASE_ID_LEN : usize = 16 ;
31+ const PAGE_TOKEN_FORMAT_VERSION : u8 = 1 ;
3032const INDEX_ENTRIES_PRIMARY_NAMESPACE : & str = "_tier_store_entries" ;
3133const INDEX_JOURNAL_PRIMARY_NAMESPACE : & str = "_tier_store_journal" ;
3234const INDEX_METADATA_PRIMARY_NAMESPACE : & str = "_tier_store_metadata" ;
@@ -98,9 +100,65 @@ impl JournalEntry {
98100 }
99101}
100102
103+ struct TierStorePageToken {
104+ format_version : u8 ,
105+ index_database_id : Vec < u8 > ,
106+ namespace_id : String ,
107+ index_page_token : String ,
108+ }
109+
110+ impl_writeable_tlv_based ! ( TierStorePageToken , {
111+ ( 0 , format_version, required) ,
112+ ( 2 , index_database_id, required) ,
113+ ( 4 , namespace_id, required) ,
114+ ( 6 , index_page_token, required) ,
115+ } ) ;
116+
117+ impl TierStorePageToken {
118+ /// Wraps an index-store page token with the context required to validate its later use.
119+ fn encode (
120+ index_database_id : & [ u8 ; INDEX_DATABASE_ID_LEN ] , namespace_id : String ,
121+ index_page_token : PageToken ,
122+ ) -> PageToken {
123+ let token = Self {
124+ format_version : PAGE_TOKEN_FORMAT_VERSION ,
125+ index_database_id : index_database_id. to_vec ( ) ,
126+ namespace_id,
127+ index_page_token : index_page_token. to_string ( ) ,
128+ } ;
129+ PageToken :: new ( Writeable :: encode ( & token) . to_lower_hex_string ( ) )
130+ }
131+
132+ /// Decodes a TierStore token and rejects tokens issued for another index or namespace.
133+ fn decode (
134+ token : PageToken , expected_database_id : & [ u8 ; INDEX_DATABASE_ID_LEN ] ,
135+ expected_namespace_id : & str ,
136+ ) -> io:: Result < PageToken > {
137+ let encoded = Vec :: from_hex ( token. as_str ( ) ) . map_err ( |_| {
138+ io:: Error :: new ( io:: ErrorKind :: InvalidInput , "Invalid TierStore page token" )
139+ } ) ?;
140+ let mut reader = & * encoded;
141+ let token: Self = Readable :: read ( & mut reader) . map_err ( |_| {
142+ io:: Error :: new ( io:: ErrorKind :: InvalidInput , "Invalid TierStore page token" )
143+ } ) ?;
144+ if !reader. is_empty ( )
145+ || token. format_version != PAGE_TOKEN_FORMAT_VERSION
146+ || token. index_database_id != expected_database_id
147+ || token. namespace_id != expected_namespace_id
148+ {
149+ return Err ( io:: Error :: new (
150+ io:: ErrorKind :: InvalidInput ,
151+ "TierStore page token does not belong to this index namespace" ,
152+ ) ) ;
153+ }
154+ Ok ( PageToken :: new ( token. index_page_token ) )
155+ }
156+ }
157+
101158pub ( crate ) struct TierStoreIndex {
102159 // Holding the store keeps its exclusive SQLite lock for the lifetime of the tier store.
103160 store : Arc < DynStore > ,
161+ database_id : [ u8 ; INDEX_DATABASE_ID_LEN ] ,
104162}
105163
106164impl TierStoreIndex {
@@ -112,14 +170,22 @@ impl TierStoreIndex {
112170 Some ( KV_TABLE_NAME . to_string ( ) ) ,
113171 ) ?;
114172 let store: Arc < DynStore > = Arc :: new ( DynStoreWrapper ( store) ) ;
115- Self :: read_or_create_database_id ( store. as_ref ( ) ) . await ?;
116- Ok ( Self { store } )
173+ let database_id = Self :: read_or_create_database_id ( store. as_ref ( ) ) . await ?;
174+ Ok ( Self { store, database_id } )
117175 }
118176
119177 /// Constructs an index over the supplied store for tests that do not need SQLite persistence.
120178 #[ cfg( test) ]
121179 fn from_store ( store : Arc < DynStore > ) -> Self {
122- Self { store }
180+ Self { store, database_id : [ 1 ; INDEX_DATABASE_ID_LEN ] }
181+ }
182+
183+ /// Constructs a test index with a specified database identity.
184+ #[ cfg( test) ]
185+ fn from_store_with_database_id (
186+ store : Arc < DynStore > , database_id : [ u8 ; INDEX_DATABASE_ID_LEN ] ,
187+ ) -> Self {
188+ Self { store, database_id }
123189 }
124190
125191 /// Derives the internal secondary namespace for a logical namespace pair.
@@ -262,17 +328,25 @@ impl TierStoreIndex {
262328 . await
263329 }
264330
265- /// Lists logical keys in the index store's creation order.
331+ /// Lists logical keys in the index store's creation order using a namespace-bound token .
266332 async fn list_paginated (
267333 & self , primary_namespace : & str , secondary_namespace : & str , page_token : Option < PageToken > ,
268334 ) -> io:: Result < PaginatedListResponse > {
269- PaginatedKVStore :: list_paginated (
335+ let namespace_id = Self :: namespace_id ( primary_namespace, secondary_namespace) ;
336+ let index_page_token = page_token
337+ . map ( |token| TierStorePageToken :: decode ( token, & self . database_id , & namespace_id) )
338+ . transpose ( ) ?;
339+ let response = PaginatedKVStore :: list_paginated (
270340 self . store . as_ref ( ) ,
271341 INDEX_ENTRIES_PRIMARY_NAMESPACE ,
272- & Self :: namespace_id ( primary_namespace , secondary_namespace ) ,
273- page_token ,
342+ & namespace_id,
343+ index_page_token ,
274344 )
275- . await
345+ . await ?;
346+ let next_page_token = response
347+ . next_page_token
348+ . map ( |token| TierStorePageToken :: encode ( & self . database_id , namespace_id, token) ) ;
349+ Ok ( PaginatedListResponse { keys : response. keys , next_page_token } )
276350 }
277351
278352 /// Persists a pending operation before its value-store effects begin.
@@ -1350,6 +1424,57 @@ mod tests {
13501424 }
13511425 }
13521426
1427+ #[ test]
1428+ fn page_token_roundtrips_and_validates_context ( ) {
1429+ let database_id = [ 2 ; INDEX_DATABASE_ID_LEN ] ;
1430+ let namespace_id = TierStoreIndex :: namespace_id ( "primary" , "secondary" ) ;
1431+ let token = TierStorePageToken :: encode (
1432+ & database_id,
1433+ namespace_id. clone ( ) ,
1434+ PageToken :: new ( "opaque:index-token" . to_string ( ) ) ,
1435+ ) ;
1436+
1437+ let decoded =
1438+ TierStorePageToken :: decode ( token. clone ( ) , & database_id, & namespace_id) . unwrap ( ) ;
1439+ assert_eq ! ( decoded. as_str( ) , "opaque:index-token" ) ;
1440+ assert_eq ! (
1441+ TierStorePageToken :: decode( token. clone( ) , & [ 3 ; INDEX_DATABASE_ID_LEN ] , & namespace_id)
1442+ . unwrap_err( )
1443+ . kind( ) ,
1444+ io:: ErrorKind :: InvalidInput
1445+ ) ;
1446+ assert_eq ! (
1447+ TierStorePageToken :: decode( token, & database_id, "another-namespace" )
1448+ . unwrap_err( )
1449+ . kind( ) ,
1450+ io:: ErrorKind :: InvalidInput
1451+ ) ;
1452+ assert_eq ! (
1453+ TierStorePageToken :: decode(
1454+ PageToken :: new( "not-a-tier-store-token" . to_string( ) ) ,
1455+ & database_id,
1456+ & namespace_id,
1457+ )
1458+ . unwrap_err( )
1459+ . kind( ) ,
1460+ io:: ErrorKind :: InvalidInput
1461+ ) ;
1462+ let unsupported_version = TierStorePageToken {
1463+ format_version : PAGE_TOKEN_FORMAT_VERSION + 1 ,
1464+ index_database_id : database_id. to_vec ( ) ,
1465+ namespace_id : namespace_id. clone ( ) ,
1466+ index_page_token : "opaque:index-token" . to_string ( ) ,
1467+ } ;
1468+ let unsupported_version =
1469+ PageToken :: new ( Writeable :: encode ( & unsupported_version) . to_lower_hex_string ( ) ) ;
1470+ assert_eq ! (
1471+ TierStorePageToken :: decode( unsupported_version, & database_id, & namespace_id)
1472+ . unwrap_err( )
1473+ . kind( ) ,
1474+ io:: ErrorKind :: InvalidInput
1475+ ) ;
1476+ }
1477+
13531478 #[ tokio:: test]
13541479 async fn index_store_is_internal_persistent_sqlite_store ( ) {
13551480 let base_dir = random_storage_path ( ) ;
@@ -1497,6 +1622,47 @@ mod tests {
14971622 assert_eq ! ( KVStore :: list( & tier, "namespace" , "" ) . await . unwrap( ) . len( ) , 55 ) ;
14981623 }
14991624
1625+ #[ tokio:: test]
1626+ async fn paginated_listing_rejects_tokens_from_another_namespace_or_index ( ) {
1627+ let base_dir = random_storage_path ( ) ;
1628+ let log_path = base_dir. join ( "tier_store_test.log" ) . to_string_lossy ( ) . into_owned ( ) ;
1629+ let logger = Arc :: new ( Logger :: new_fs_writer ( log_path, Level :: Trace ) . unwrap ( ) ) ;
1630+ let _cleanup = CleanupDir ( base_dir) ;
1631+
1632+ let primary_store: Arc < DynStore > = Arc :: new ( DynStoreWrapper ( InMemoryStore :: new ( ) ) ) ;
1633+ let mut tier = setup_tier_store ( Arc :: clone ( & primary_store) , Arc :: clone ( & logger) ) ;
1634+ let index_store: Arc < DynStore > = Arc :: new ( DynStoreWrapper ( InMemoryStore :: new ( ) ) ) ;
1635+ tier. set_index_store ( TierStoreIndex :: from_store_with_database_id (
1636+ index_store,
1637+ [ 1 ; INDEX_DATABASE_ID_LEN ] ,
1638+ ) ) ;
1639+ for i in 0 ..51 {
1640+ tier. write ( "namespace" , "" , & format ! ( "key-{i:02}" ) , vec ! [ 1 ] ) . await . unwrap ( ) ;
1641+ }
1642+ let token = PaginatedKVStore :: list_paginated ( & tier, "namespace" , "" , None )
1643+ . await
1644+ . unwrap ( )
1645+ . next_page_token
1646+ . unwrap ( ) ;
1647+
1648+ let namespace_error =
1649+ PaginatedKVStore :: list_paginated ( & tier, "other-namespace" , "" , Some ( token. clone ( ) ) )
1650+ . await
1651+ . unwrap_err ( ) ;
1652+ assert_eq ! ( namespace_error. kind( ) , io:: ErrorKind :: InvalidInput ) ;
1653+
1654+ let replacement_index_store: Arc < DynStore > =
1655+ Arc :: new ( DynStoreWrapper ( InMemoryStore :: new ( ) ) ) ;
1656+ tier. set_index_store ( TierStoreIndex :: from_store_with_database_id (
1657+ replacement_index_store,
1658+ [ 2 ; INDEX_DATABASE_ID_LEN ] ,
1659+ ) ) ;
1660+ let index_error = PaginatedKVStore :: list_paginated ( & tier, "namespace" , "" , Some ( token) )
1661+ . await
1662+ . unwrap_err ( ) ;
1663+ assert_eq ! ( index_error. kind( ) , io:: ErrorKind :: InvalidInput ) ;
1664+ }
1665+
15001666 #[ tokio:: test]
15011667 async fn pending_creates_roll_forward_to_primary_and_backup_in_journal_order ( ) {
15021668 let base_dir = random_storage_path ( ) ;
0 commit comments