@@ -23,6 +23,32 @@ mod tests {
2323 SqliteShardStore :: new ( conn) . expect ( "create store" )
2424 }
2525
26+ fn legacy_schema_conn ( ) -> Connection {
27+ let conn = Connection :: open_in_memory ( ) . expect ( "open legacy in-memory sqlite" ) ;
28+ conn. execute_batch (
29+ "CREATE TABLE commitment_tree_shards (
30+ shard_index INTEGER PRIMARY KEY,
31+ shard_data BLOB NOT NULL
32+ );
33+ CREATE TABLE commitment_tree_cap (
34+ id INTEGER PRIMARY KEY CHECK (id = 0),
35+ cap_data BLOB NOT NULL
36+ );
37+ CREATE TABLE commitment_tree_checkpoints (
38+ checkpoint_id INTEGER PRIMARY KEY,
39+ position INTEGER
40+ );
41+ CREATE TABLE commitment_tree_checkpoint_marks_removed (
42+ checkpoint_id INTEGER NOT NULL,
43+ position INTEGER NOT NULL,
44+ PRIMARY KEY (checkpoint_id, position),
45+ FOREIGN KEY (checkpoint_id) REFERENCES commitment_tree_checkpoints(checkpoint_id)
46+ );" ,
47+ )
48+ . expect ( "create legacy schema" ) ;
49+ conn
50+ }
51+
2652 fn test_hash ( i : u8 ) -> MerkleHashOrchard {
2753 let empty = MerkleHashOrchard :: empty_leaf ( ) ;
2854 MerkleHashOrchard :: combine ( Level :: from ( i % 31 + 1 ) , & empty, & empty)
@@ -80,6 +106,68 @@ mod tests {
80106 assert_eq ! ( count, 1 ) ;
81107 }
82108
109+ #[ test]
110+ fn test_schema_rejects_negative_shard_index ( ) {
111+ let store = test_store ( ) ;
112+ let data = serialize_tree ( & Tree :: leaf ( ( test_hash ( 1 ) , RetentionFlags :: MARKED ) ) ) ;
113+
114+ let err = store
115+ . with_conn ( |conn| {
116+ conn. execute (
117+ "INSERT INTO commitment_tree_shards (shard_index, shard_data) VALUES (?1, ?2)" ,
118+ rusqlite:: params![ -1i64 , data] ,
119+ )
120+ } )
121+ . expect_err ( "negative shard index should be rejected" ) ;
122+
123+ assert ! (
124+ err. to_string( ) . contains( "CHECK constraint failed" ) ,
125+ "unexpected sqlite error: {err}"
126+ ) ;
127+ }
128+
129+ #[ test]
130+ fn test_schema_rejects_negative_checkpoint_positions ( ) {
131+ let store = test_store ( ) ;
132+
133+ let err = store
134+ . with_conn ( |conn| {
135+ conn. execute (
136+ "INSERT INTO commitment_tree_checkpoints (checkpoint_id, position) VALUES (?1, ?2)" ,
137+ rusqlite:: params![ 1u32 , -1i64 ] ,
138+ )
139+ } )
140+ . expect_err ( "negative checkpoint position should be rejected" ) ;
141+
142+ assert ! (
143+ err. to_string( ) . contains( "CHECK constraint failed" ) ,
144+ "unexpected sqlite error: {err}"
145+ ) ;
146+
147+ store
148+ . with_conn ( |conn| {
149+ conn. execute (
150+ "INSERT INTO commitment_tree_checkpoints (checkpoint_id, position) VALUES (?1, NULL)" ,
151+ rusqlite:: params![ 2u32 ] ,
152+ )
153+ } )
154+ . expect ( "insert empty checkpoint" ) ;
155+
156+ let err = store
157+ . with_conn ( |conn| {
158+ conn. execute (
159+ "INSERT INTO commitment_tree_checkpoint_marks_removed (checkpoint_id, position) VALUES (?1, ?2)" ,
160+ rusqlite:: params![ 2u32 , -1i64 ] ,
161+ )
162+ } )
163+ . expect_err ( "negative mark position should be rejected" ) ;
164+
165+ assert ! (
166+ err. to_string( ) . contains( "CHECK constraint failed" ) ,
167+ "unexpected sqlite error: {err}"
168+ ) ;
169+ }
170+
83171 // -- Serialization round-trip tests --
84172
85173 #[ test]
@@ -264,6 +352,48 @@ mod tests {
264352 assert_eq ! ( roots. last( ) . expect( "last root" ) . index( ) , 2 ) ;
265353 }
266354
355+ #[ test]
356+ fn test_last_shard_rejects_negative_shard_index_from_legacy_schema ( ) {
357+ let conn = legacy_schema_conn ( ) ;
358+ let data = serialize_tree ( & Tree :: leaf ( ( test_hash ( 7 ) , RetentionFlags :: MARKED ) ) ) ;
359+ conn. execute (
360+ "INSERT INTO commitment_tree_shards (shard_index, shard_data) VALUES (?1, ?2)" ,
361+ rusqlite:: params![ -1i64 , data] ,
362+ )
363+ . expect ( "insert corrupt shard" ) ;
364+
365+ let store = SqliteShardStore :: new ( conn) . expect ( "open legacy store" ) ;
366+ let err = store
367+ . last_shard ( )
368+ . expect_err ( "negative shard index should fail" ) ;
369+ assert ! (
370+ err. to_string( )
371+ . contains( "invalid negative shard_index in sqlite row: -1" ) ,
372+ "unexpected store error: {err}"
373+ ) ;
374+ }
375+
376+ #[ test]
377+ fn test_get_shard_roots_rejects_negative_shard_index_from_legacy_schema ( ) {
378+ let conn = legacy_schema_conn ( ) ;
379+ let data = serialize_tree ( & Tree :: leaf ( ( test_hash ( 8 ) , RetentionFlags :: MARKED ) ) ) ;
380+ conn. execute (
381+ "INSERT INTO commitment_tree_shards (shard_index, shard_data) VALUES (?1, ?2)" ,
382+ rusqlite:: params![ -1i64 , data] ,
383+ )
384+ . expect ( "insert corrupt shard" ) ;
385+
386+ let store = SqliteShardStore :: new ( conn) . expect ( "open legacy store" ) ;
387+ let err = store
388+ . get_shard_roots ( )
389+ . expect_err ( "negative shard index should fail" ) ;
390+ assert ! (
391+ err. to_string( )
392+ . contains( "invalid negative shard_index in sqlite row: -1" ) ,
393+ "unexpected store error: {err}"
394+ ) ;
395+ }
396+
267397 // -- Cap tests --
268398
269399 #[ test]
@@ -370,6 +500,51 @@ mod tests {
370500 assert_eq ! ( retrieved. tree_state( ) , TreeState :: Empty ) ;
371501 }
372502
503+ #[ test]
504+ fn test_get_checkpoint_rejects_negative_position_from_legacy_schema ( ) {
505+ let conn = legacy_schema_conn ( ) ;
506+ conn. execute (
507+ "INSERT INTO commitment_tree_checkpoints (checkpoint_id, position) VALUES (?1, ?2)" ,
508+ rusqlite:: params![ 1u32 , -1i64 ] ,
509+ )
510+ . expect ( "insert corrupt checkpoint" ) ;
511+
512+ let store = SqliteShardStore :: new ( conn) . expect ( "open legacy store" ) ;
513+ let err = store
514+ . get_checkpoint ( & 1 )
515+ . expect_err ( "negative checkpoint position should fail" ) ;
516+ assert ! (
517+ err. to_string( )
518+ . contains( "invalid negative position in sqlite row: -1" ) ,
519+ "unexpected store error: {err}"
520+ ) ;
521+ }
522+
523+ #[ test]
524+ fn test_get_checkpoint_rejects_negative_mark_position_from_legacy_schema ( ) {
525+ let conn = legacy_schema_conn ( ) ;
526+ conn. execute (
527+ "INSERT INTO commitment_tree_checkpoints (checkpoint_id, position) VALUES (?1, ?2)" ,
528+ rusqlite:: params![ 1u32 , 10i64 ] ,
529+ )
530+ . expect ( "insert checkpoint" ) ;
531+ conn. execute (
532+ "INSERT INTO commitment_tree_checkpoint_marks_removed (checkpoint_id, position) VALUES (?1, ?2)" ,
533+ rusqlite:: params![ 1u32 , -1i64 ] ,
534+ )
535+ . expect ( "insert corrupt mark" ) ;
536+
537+ let store = SqliteShardStore :: new ( conn) . expect ( "open legacy store" ) ;
538+ let err = store
539+ . get_checkpoint ( & 1 )
540+ . expect_err ( "negative mark position should fail" ) ;
541+ assert ! (
542+ err. to_string( )
543+ . contains( "invalid negative position in sqlite row: -1" ) ,
544+ "unexpected store error: {err}"
545+ ) ;
546+ }
547+
373548 #[ test]
374549 fn test_remove_checkpoint ( ) {
375550 let mut store = test_store ( ) ;
0 commit comments