fix(trie): Return full_key from update_leaves unless it is not a child of the missing path (#21699)

Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
Brian Picciano
2026-02-02 12:07:56 +01:00
committed by GitHub
parent f74e594292
commit 49128ed28f

View File

@@ -1237,8 +1237,8 @@ impl SparseTrieExt for ParallelSparseTrie {
Ok(()) => {}
Err(e) => {
if let Some(path) = Self::get_retriable_path(&e) {
let target_key = Self::nibbles_to_padded_b256(&path);
let min_len = (path.len() as u8).min(64);
let (target_key, min_len) =
Self::proof_target_for_path(key, &full_path, &path);
proof_required_fn(target_key, min_len);
updates.insert(key, LeafUpdate::Changed(value));
} else {
@@ -1251,8 +1251,8 @@ impl SparseTrieExt for ParallelSparseTrie {
if let Err(e) = self.update_leaf(full_path, value.clone(), NoRevealProvider)
{
if let Some(path) = Self::get_retriable_path(&e) {
let target_key = Self::nibbles_to_padded_b256(&path);
let min_len = (path.len() as u8).min(64);
let (target_key, min_len) =
Self::proof_target_for_path(key, &full_path, &path);
proof_required_fn(target_key, min_len);
updates.insert(key, LeafUpdate::Changed(value));
} else {
@@ -1265,8 +1265,8 @@ impl SparseTrieExt for ParallelSparseTrie {
// Touched is read-only: check if path is accessible, request proof if blinded.
match self.find_leaf(&full_path, None) {
Err(LeafLookupError::BlindedNode { path, .. }) => {
let target_key = Self::nibbles_to_padded_b256(&path);
let min_len = (path.len() as u8).min(64);
let (target_key, min_len) =
Self::proof_target_for_path(key, &full_path, &path);
proof_required_fn(target_key, min_len);
updates.insert(key, LeafUpdate::Touched);
}
@@ -1339,12 +1339,23 @@ impl ParallelSparseTrie {
/// Converts a nibbles path to a B256, right-padding with zeros to 64 nibbles.
fn nibbles_to_padded_b256(path: &Nibbles) -> B256 {
let packed = path.pack();
let mut bytes = [0u8; 32];
bytes[..packed.len()].copy_from_slice(&packed);
path.pack_to(&mut bytes);
B256::from(bytes)
}
/// Computes the proof target key and `min_len` for a blinded node error.
///
/// Returns `(target_key, min_len)` where:
/// - `target_key` is `full_key` if `path` is a prefix of `full_path`, otherwise the padded path
/// - `min_len` is always based on `path.len()`
fn proof_target_for_path(full_key: B256, full_path: &Nibbles, path: &Nibbles) -> (B256, u8) {
let min_len = (path.len() as u8).min(64);
let target_key =
if full_path.starts_with(path) { full_key } else { Self::nibbles_to_padded_b256(path) };
(target_key, min_len)
}
/// Rolls back a partial update by removing the value, removing any inserted nodes,
/// removing any inserted branch masks, and restoring any modified original node.
/// This ensures `update_leaf` is atomic - either it succeeds completely or leaves the trie
@@ -8860,4 +8871,28 @@ mod tests {
"Value count should be unchanged after atomic failure (no dangling values)"
);
}
#[test]
fn test_nibbles_to_padded_b256() {
// Empty nibbles should produce all zeros
let empty = Nibbles::default();
assert_eq!(ParallelSparseTrie::nibbles_to_padded_b256(&empty), B256::ZERO);
// Full 64-nibble path should round-trip through B256
let full_key = b256!("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
let full_nibbles = Nibbles::unpack(full_key);
assert_eq!(ParallelSparseTrie::nibbles_to_padded_b256(&full_nibbles), full_key);
// Partial nibbles should be left-aligned with zero padding on the right
// 4 nibbles [0x1, 0x2, 0x3, 0x4] should pack to 0x1234...00
let partial = Nibbles::from_nibbles_unchecked([0x1, 0x2, 0x3, 0x4]);
let expected = b256!("1234000000000000000000000000000000000000000000000000000000000000");
assert_eq!(ParallelSparseTrie::nibbles_to_padded_b256(&partial), expected);
// Single nibble
let single = Nibbles::from_nibbles_unchecked([0xf]);
let expected_single =
b256!("f000000000000000000000000000000000000000000000000000000000000000");
assert_eq!(ParallelSparseTrie::nibbles_to_padded_b256(&single), expected_single);
}
}