Skip to content

Commit 02f5f70

Browse files
fix: manage node patch serialization for diffs range [1, 256], #6593
1 parent bdcf714 commit 02f5f70

File tree

2 files changed

+182
-8
lines changed

2 files changed

+182
-8
lines changed

stackslib/src/chainstate/stacks/index/node.rs

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,20 +1113,50 @@ impl fmt::Debug for TrieNodePatch {
11131113
}
11141114

11151115
impl StacksMessageCodec for TrieNodePatch {
1116+
/// Serializes this [`TrieNodePatch`] to the given writer, with the following format:
1117+
///
1118+
/// 0 1 1+P 2+P 2+P+N
1119+
/// |----|--------|----------|----------------|
1120+
/// id ptr diff len ptr diffs
1121+
/// (1) (P) (1) (N)
1122+
///
1123+
/// where:
1124+
/// - `id` is [`TrieNodeID::Patch`]
1125+
/// - `ptr` is a compressed [`TriePtr`]
1126+
/// - `diff len` is the number of diffs, serialized as `len - 1`
1127+
/// - `ptr diffs` are the patch diffs written in compressed format
1128+
///
1129+
/// # Invariants
1130+
///
1131+
/// The number of diffs must be in the range `1..=256`. A patch is valid only
1132+
/// if it contains at least one diff (see the factory methods
1133+
/// [`TrieNodePatch::try_from_nodetype`] and [`TrieNodePatch::try_from_patch`]).
1134+
///
1135+
/// To fit in a `u8`, the diff count is normalized to `len - 1` before
1136+
/// serialization.
1137+
///
1138+
/// # Errors
1139+
///
1140+
/// Returns `Err(codec_error::SerializeError)` if:
1141+
/// * Writing to `fd` fails.
1142+
/// * `ptr` fails to serialize.
1143+
/// * Any pointer in `ptr diffs` fails to serialize.
1144+
/// * The diff count is `0` or greater than `256`.
11161145
fn consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), codec_error> {
11171146
write_next(fd, &(TrieNodeID::Patch as u8))?;
11181147
self.ptr
11191148
.write_bytes_compressed(fd)
11201149
.map_err(|e| codec_error::SerializeError(format!("Failed to serialize .ptr: {e:?}")))?;
11211150

11221151
let num_ptrs = self.ptr_diff.len();
1123-
if num_ptrs >= 256 {
1124-
return Err(codec_error::SerializeError(
1125-
"Cannot serialize TrieNodePatch with more than 256 ptrs".to_string(),
1126-
));
1152+
if num_ptrs == 0 || num_ptrs > 256 {
1153+
return Err(codec_error::SerializeError(format!(
1154+
"Cannot serialize TrieNodePatch with invalid ptrs len {num_ptrs} (expected 1..=256)"
1155+
)));
11271156
}
1128-
// SAFETY: checked that num_ptrs < 256
1129-
let num_ptrs_u8 = u8::try_from(num_ptrs).expect("infallible");
1157+
// normalize num_ptrs to range [0, 255] to fit in u8
1158+
let num_ptrs_norm = num_ptrs.checked_sub(1).expect("infallible");
1159+
let num_ptrs_u8 = u8::try_from(num_ptrs_norm).expect("infallible");
11301160
write_next(fd, &num_ptrs_u8).map_err(|e| {
11311161
codec_error::SerializeError(format!("Failed to serialize .ptr_diff.len(): {e:?}"))
11321162
})?;
@@ -1139,6 +1169,22 @@ impl StacksMessageCodec for TrieNodePatch {
11391169
Ok(())
11401170
}
11411171

1172+
/// Deserializes a [`TrieNodePatch`] from the given reader.
1173+
///
1174+
/// This method expects the byte stream to be in the exact format produced by
1175+
/// [`TrieNodePatch::consensus_serialize`] (see that method for the detailed
1176+
/// wire format description)
1177+
///
1178+
/// During deserialization, the stored diff length is de-normalized by
1179+
/// adding `1`, reversing the `len - 1` normalization applied during
1180+
/// serialization.
1181+
///
1182+
/// # Errors
1183+
///
1184+
/// Returns `Err(codec_error::DeserializeError)` if:
1185+
/// * The node identifier does not match [`TrieNodeID::Patch`].
1186+
/// * Reading from `fd` fails.
1187+
/// * The pointer or any pointer diff fails to deserialize.
11421188
fn consensus_deserialize<R: Read>(fd: &mut R) -> Result<Self, codec_error> {
11431189
let id: u8 = read_next(fd)?;
11441190
if id != TrieNodeID::Patch as u8 {
@@ -1148,8 +1194,11 @@ impl StacksMessageCodec for TrieNodePatch {
11481194
}
11491195

11501196
let ptr = TriePtr::read_bytes_compressed(fd)?;
1151-
let num_ptrs: u8 = read_next(fd)?;
1152-
let num_ptrs = usize::try_from(num_ptrs).expect("infallible");
1197+
let num_ptrs_u8: u8 = read_next(fd)?;
1198+
let num_ptrs_norm = usize::try_from(num_ptrs_u8).expect("infallible");
1199+
// denormalize num_ptrs to range [1, 256] (reversing the -1 introduced during serialization)
1200+
let num_ptrs = num_ptrs_norm.checked_add(1).expect("infallible");
1201+
11531202
let mut ptr_diff: Vec<TriePtr> = Vec::with_capacity(num_ptrs);
11541203
for _ in 0..num_ptrs {
11551204
ptr_diff.push(TriePtr::read_bytes_compressed(fd)?);
@@ -1301,6 +1350,7 @@ impl TrieNodePatch {
13011350
new_node: &TrieNodeType,
13021351
) -> Option<Self> {
13031352
if clear_ctrl_bits(old_node.id()) != clear_ctrl_bits(new_node.id()) {
1353+
trace!("Cannot produce TrieNodePatch: old node and new node are not the same type!");
13041354
return None;
13051355
}
13061356

@@ -1320,9 +1370,11 @@ impl TrieNodePatch {
13201370
(_, _) => None,
13211371
};
13221372
let Some(patch) = patch_opt else {
1373+
trace!("Cannot produce TrieNodePatch: old node and new node are type leaf!");
13231374
return None;
13241375
};
13251376
if patch.ptr_diff.len() == 0 {
1377+
trace!("Cannot produce TrieNodePatch: patch has no diffs!");
13261378
return None;
13271379
}
13281380
Some(patch)
@@ -1335,6 +1387,7 @@ impl TrieNodePatch {
13351387
new_node: &TrieNodeType,
13361388
) -> Option<Self> {
13371389
if clear_ctrl_bits(old_patch.ptr.id) != clear_ctrl_bits(new_node.id()) {
1390+
trace!("Cannot produce TrieNodePatch: old node and new node are not the same type!");
13381391
return None;
13391392
}
13401393

@@ -1344,6 +1397,7 @@ impl TrieNodePatch {
13441397
ptr_diff,
13451398
};
13461399
if patch.ptr_diff.len() == 0 {
1400+
trace!("Cannot produce TrieNodePatch: patch has no diffs!");
13471401
return None;
13481402
}
13491403
return Some(patch);

stackslib/src/chainstate/stacks/index/test/node.rs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use std::io::Cursor;
2121

2222
use super::*;
2323
use crate::chainstate::stacks::index::*;
24+
use crate::codec::{Error as codec_error, StacksMessageCodec};
2425

2526
#[test]
2627
fn trieptr_to_bytes() {
@@ -5043,3 +5044,122 @@ fn trie_cursor_walk_32() {
50435044

50445045
dump_trie(&mut trie_io);
50455046
}
5047+
5048+
#[test]
5049+
fn trie_node_patch_try_from_nodetype_returns_none_when_no_diffs() {
5050+
let node = TrieNodeType::Node4(TrieNode4::new(&[1]));
5051+
5052+
let old_node_ptr = TriePtr::default();
5053+
let old_node = &node;
5054+
let new_node = &node;
5055+
let result = TrieNodePatch::try_from_nodetype(old_node_ptr, old_node, new_node);
5056+
5057+
assert!(
5058+
result.is_none(),
5059+
"None because the computed patch has no diffs"
5060+
);
5061+
}
5062+
5063+
#[test]
5064+
fn trie_node_patch_try_from_patch_returns_none_when_no_diffs() {
5065+
let old_patch_ptr = TriePtr::new(TrieNodeID::Node4 as u8, 0, 0);
5066+
let old_patch = TrieNodePatch {
5067+
ptr: old_patch_ptr.clone(),
5068+
ptr_diff: vec![],
5069+
};
5070+
let new_node = TrieNodeType::Node4(TrieNode4::new(&[1]));
5071+
let result = TrieNodePatch::try_from_patch(old_patch_ptr, &old_patch, &new_node);
5072+
5073+
assert!(
5074+
result.is_none(),
5075+
"None because the computed patch has no diffs"
5076+
);
5077+
}
5078+
5079+
#[test]
5080+
fn trie_node_patch_serialize_ok() {
5081+
let patch_node = TrieNodePatch {
5082+
ptr: TriePtr::new(1, 10, 0),
5083+
ptr_diff: vec![TriePtr::new(1, 20, 0).clone(); 1],
5084+
};
5085+
5086+
let mut buffer = Cursor::new(Vec::new());
5087+
patch_node
5088+
.consensus_serialize(&mut buffer)
5089+
.expect("serialization should be ok");
5090+
5091+
// To fit in 1 byte, diff count is serialized 0-based (where 0 => 1 and 255 => 256)
5092+
let diff_count = 0u8;
5093+
assert_eq!(
5094+
vec![6, 65, 10, 0, 0, 0, 0, diff_count, 65, 20, 0, 0, 0, 0],
5095+
buffer.into_inner(),
5096+
);
5097+
}
5098+
5099+
#[test]
5100+
fn trie_node_patch_serialize_fails_with_ptr_diffs_len_0() {
5101+
let patch_node = TrieNodePatch {
5102+
ptr: TriePtr::default(),
5103+
ptr_diff: vec![],
5104+
};
5105+
5106+
let mut buffer = Cursor::new(Vec::new());
5107+
let error = patch_node
5108+
.consensus_serialize(&mut buffer)
5109+
.expect_err("serialization should fail");
5110+
5111+
assert!(
5112+
matches!(&error, codec_error::SerializeError(msg) if msg.contains("len 0")),
5113+
"instead got: {error}"
5114+
);
5115+
}
5116+
5117+
#[test]
5118+
fn trie_node_patch_serialize_ok_with_ptr_diffs_len_256() {
5119+
let patch_node = TrieNodePatch {
5120+
ptr: TriePtr::default(),
5121+
ptr_diff: vec![TriePtr::default(); 256],
5122+
};
5123+
5124+
let mut buffer = Cursor::new(Vec::new());
5125+
let result = patch_node.consensus_serialize(&mut buffer);
5126+
assert!(
5127+
result.is_ok(),
5128+
"Got Error: {}",
5129+
result.unwrap_err().to_string()
5130+
);
5131+
}
5132+
5133+
#[test]
5134+
fn trie_node_patch_serialize_fails_with_ptr_diffs_len_257() {
5135+
let patch_node = TrieNodePatch {
5136+
ptr: TriePtr::default(),
5137+
ptr_diff: vec![TriePtr::default(); 257],
5138+
};
5139+
5140+
let mut buffer = Cursor::new(Vec::new());
5141+
let error = patch_node
5142+
.consensus_serialize(&mut buffer)
5143+
.expect_err("serialization should fail");
5144+
5145+
assert!(
5146+
matches!(&error, codec_error::SerializeError(msg) if msg.contains("len 257")),
5147+
"instead got: {error}"
5148+
);
5149+
}
5150+
5151+
#[test]
5152+
fn trie_node_patch_deserialize_ok_with_ptr_diffs_len_1() {
5153+
// To fit in 1 byte, diff count is serialized 0-based (where 0 => 1 and 255 => 256)
5154+
let diff_count = 0u8;
5155+
let mut buffer = Cursor::new(vec![6, 65, 10, 0, 0, 0, 0, diff_count, 65, 20, 0, 0, 0, 0]);
5156+
5157+
let patch_node =
5158+
TrieNodePatch::consensus_deserialize(&mut buffer).expect("deserialization should be ok");
5159+
5160+
let expected = TrieNodePatch {
5161+
ptr: TriePtr::new(1, 10, 0),
5162+
ptr_diff: vec![TriePtr::new(1, 20, 0); 1],
5163+
};
5164+
assert_eq!(expected, patch_node);
5165+
}

0 commit comments

Comments
 (0)