Skip to content

Commit 5ca122d

Browse files
committed
fix(commitment-tree): reject negative sqlite positions
1 parent 9f67e8c commit 5ca122d

2 files changed

Lines changed: 226 additions & 17 deletions

File tree

grovedb-commitment-tree/src/client/sqlite_store/sql_helpers.rs

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,18 @@ use super::{
1818
SqliteShardStoreError, SHARD_HEIGHT,
1919
};
2020

21+
fn non_negative_i64_to_u64(value_name: &str, value: i64) -> Result<u64, SqliteShardStoreError> {
22+
u64::try_from(value).map_err(|_| {
23+
SqliteShardStoreError::Serialization(format!(
24+
"invalid negative {value_name} in sqlite row: {value}"
25+
))
26+
})
27+
}
28+
2129
pub(crate) fn create_tables(conn: &Connection) -> Result<(), SqliteShardStoreError> {
2230
conn.execute_batch(
2331
"CREATE TABLE IF NOT EXISTS commitment_tree_shards (
24-
shard_index INTEGER PRIMARY KEY,
32+
shard_index INTEGER PRIMARY KEY CHECK (shard_index >= 0),
2533
shard_data BLOB NOT NULL
2634
);
2735
CREATE TABLE IF NOT EXISTS commitment_tree_cap (
@@ -30,11 +38,11 @@ pub(crate) fn create_tables(conn: &Connection) -> Result<(), SqliteShardStoreErr
3038
);
3139
CREATE TABLE IF NOT EXISTS commitment_tree_checkpoints (
3240
checkpoint_id INTEGER PRIMARY KEY,
33-
position INTEGER
41+
position INTEGER CHECK (position IS NULL OR position >= 0)
3442
);
3543
CREATE TABLE IF NOT EXISTS commitment_tree_checkpoint_marks_removed (
3644
checkpoint_id INTEGER NOT NULL,
37-
position INTEGER NOT NULL,
45+
position INTEGER NOT NULL CHECK (position >= 0),
3846
PRIMARY KEY (checkpoint_id, position),
3947
FOREIGN KEY (checkpoint_id) REFERENCES commitment_tree_checkpoints(checkpoint_id)
4048
);",
@@ -85,7 +93,8 @@ pub(crate) fn sql_last_shard(
8593
match row {
8694
None => Ok(None),
8795
Some((index, data)) => {
88-
let addr = Address::from_parts(Level::from(SHARD_HEIGHT), index as u64);
96+
let index = non_negative_i64_to_u64("shard_index", index)?;
97+
let addr = Address::from_parts(Level::from(SHARD_HEIGHT), index);
8998
let mut pos = 0;
9099
let tree = deserialize_tree(&data, &mut pos)?;
91100
let located = LocatedTree::from_parts(addr, tree).map_err(|addr| {
@@ -116,13 +125,24 @@ pub(crate) fn sql_get_shard_roots(
116125
) -> Result<Vec<Address>, SqliteShardStoreError> {
117126
let mut stmt =
118127
conn.prepare("SELECT shard_index FROM commitment_tree_shards ORDER BY shard_index")?;
119-
let rows = stmt.query_map([], |row| {
120-
let index: i64 = row.get(0)?;
121-
Ok(Address::from_parts(Level::from(SHARD_HEIGHT), index as u64))
122-
})?;
128+
let rows = stmt
129+
.query_map([], |row| row.get::<_, i64>(0))
130+
.map_err(|err| {
131+
SqliteShardStoreError::Serialization(format!(
132+
"failed to query shard roots from sqlite: {err}"
133+
))
134+
})?;
123135
let mut result = Vec::new();
124-
for addr in rows {
125-
result.push(addr?);
136+
for index in rows {
137+
let index = index.map_err(|err| {
138+
SqliteShardStoreError::Serialization(format!(
139+
"failed to read shard root row from sqlite: {err}"
140+
))
141+
})?;
142+
result.push(Address::from_parts(
143+
Level::from(SHARD_HEIGHT),
144+
non_negative_i64_to_u64("shard_index", index)?,
145+
));
126146
}
127147
Ok(result)
128148
}
@@ -375,18 +395,32 @@ fn sql_load_checkpoint(
375395
) -> Result<Checkpoint, SqliteShardStoreError> {
376396
let tree_state = match position {
377397
None => TreeState::Empty,
378-
Some(p) => TreeState::AtPosition(Position::from(p as u64)),
398+
Some(p) => TreeState::AtPosition(Position::from(non_negative_i64_to_u64("position", p)?)),
379399
};
380400

381401
let mut stmt = conn.prepare(
382402
"SELECT position FROM commitment_tree_checkpoint_marks_removed WHERE checkpoint_id = ?1",
383403
)?;
384-
let marks: BTreeSet<Position> = stmt
385-
.query_map(params![checkpoint_id], |row| {
386-
let p: i64 = row.get(0)?;
387-
Ok(Position::from(p as u64))
388-
})?
389-
.collect::<Result<BTreeSet<_>, _>>()?;
404+
let rows = stmt
405+
.query_map(params![checkpoint_id], |row| row.get::<_, i64>(0))
406+
.map_err(|err| {
407+
SqliteShardStoreError::Serialization(format!(
408+
"failed to query checkpoint removed marks from sqlite for checkpoint \
409+
{checkpoint_id}: {err}"
410+
))
411+
})?;
412+
let mut marks = BTreeSet::new();
413+
for position in rows {
414+
let position = position.map_err(|err| {
415+
SqliteShardStoreError::Serialization(format!(
416+
"failed to read checkpoint removed mark row from sqlite for checkpoint \
417+
{checkpoint_id}: {err}"
418+
))
419+
})?;
420+
marks.insert(Position::from(non_negative_i64_to_u64(
421+
"position", position,
422+
)?));
423+
}
390424

391425
Ok(Checkpoint::from_parts(tree_state, marks))
392426
}

grovedb-commitment-tree/src/client/sqlite_store_tests.rs

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)