Skip to content

Commit

Permalink
add random tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mina86 committed Aug 5, 2023
1 parent 12c6fd6 commit 55e3dc2
Show file tree
Hide file tree
Showing 8 changed files with 389 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,4 @@ jobs:
run: cargo test

- name: Run tests with Miri
run: cargo miri test
run: STRESS_TEST_ITERATIONS=5 cargo miri test
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ resolver = "2"
[workspace.dependencies]
base64 = { version = "0.21", default-features = false, features = ["alloc"] }
derive_more = "0.99.17"
sha2 = { version = "0.10.7", default-features = false }
pretty_assertions = "1.4.0"
rand = { version = "0.8.5" }
sha2 = { version = "0.10.7", default-features = false }
1 change: 1 addition & 0 deletions sealable-trie/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ sha2.workspace = true

[dev-dependencies]
pretty_assertions.workspace = true
rand.workspace = true
6 changes: 3 additions & 3 deletions sealable-trie/src/bits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ impl<'a> Slice<'a> {
}
let has_bits =
u32::try_from(bytes.len()).unwrap_or(u32::MAX).saturating_mul(8);
if length == 0 || (u32::from(length) + u32::from(offset) <= has_bits) {
if u32::from(length) + u32::from(offset) <= has_bits {
Some(Self {
offset,
length,
Expand Down Expand Up @@ -104,7 +104,7 @@ impl<'a> Slice<'a> {
let (offset, length) = (num % 8, num / 8);
debug_assert!(
length != 0 &&
usize::from(length + offset + 7) / 8 <
usize::from(length + offset + 7) / 8 <=
nodes::MAX_EXTENSION_KEY_SIZE,
"offset:{offset} length:{length}"
);
Expand Down Expand Up @@ -240,7 +240,7 @@ impl<'a> Slice<'a> {
/// assert_eq!(bits::Slice::new(&[0x20], 2, 2).unwrap(), slice);
///
/// assert!(slice.strip_prefix(slice.clone()));
/// assert_eq!(bits::Slice::new(&[], 4, 0).unwrap(), slice);
/// assert_eq!(bits::Slice::new(&[0x00], 4, 0).unwrap(), slice);
/// ```
pub fn strip_prefix(&mut self, prefix: Slice<'_>) -> bool {
if self.offset != prefix.offset || self.length < prefix.length {
Expand Down
2 changes: 1 addition & 1 deletion sealable-trie/src/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ impl Ptr {
/// Two most significant bits of the address are masked out thus ensuring
/// that the value is never too large.
pub(crate) fn new_truncated(ptr: u32) -> Option<Ptr> {
NonZeroU32::new(ptr & 0x3FFF_FFF).map(Self)
NonZeroU32::new(ptr & (u32::MAX >> 2)).map(Self)
}
}

Expand Down
131 changes: 122 additions & 9 deletions sealable-trie/src/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use crate::stdx;

#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_rand;

pub(crate) const MAX_EXTENSION_KEY_SIZE: usize = 34;

Expand Down Expand Up @@ -77,7 +79,7 @@ pub enum Node<'a, R: AsReference<'a> = RawRef<'a>> {
// 36-byte array which holds the key extension. Only `o..o+k` bits in it
// are the actual key; others are set to zero.
//
// Value: 1100_0000 0000_0000 0000_0000 0000_000s <vhash> <node-ref>
// Value: 1100_0000 0000_0000 0000_0000 0000_0000 <vhash> <node-ref>
// <vhash> is the hash of the stored value. `s` is zero if the value hasn’t
// been sealed or one otherwise.
//
Expand All @@ -103,7 +105,7 @@ pub enum Node<'a, R: AsReference<'a> = RawRef<'a>> {
// The actual pointer value is therefore 30-bit long.
//
// TODO(mina86): Implement handling of sealed values.
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, PartialEq, derive_more::Deref)]
#[repr(transparent)]
pub struct RawNode(pub(crate) [u8; 72]);

Expand All @@ -124,7 +126,7 @@ pub struct RawNode(pub(crate) [u8; 72]);
// If the node is also a prefix of another key, <hash> is hash of the node
// that continues the key. Otherwise it’s not present.
// ```
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, PartialEq, derive_more::Deref)]
pub struct ProofNode(Box<[u8]>);

/// Node reference as parsed from the raw node representation. It can either
Expand Down Expand Up @@ -322,11 +324,18 @@ impl<'a> RawRef<'a> {
let ptr = u32::from_be_bytes(*ptr);
let hash = hash.into();
if ptr & 0x4000_0000 == 0 {
debug_assert_eq!(0, ptr & 0xC000_0000);
debug_assert_eq!(
0,
ptr & 0xC000_0000,
"Failed decoding RawRef: {bytes:?}"
);
let ptr = Ptr::new_truncated(ptr);
Self::Node { ptr, hash }
} else {
debug_assert_eq!(0x4000_0000, ptr);
debug_assert_eq!(
0x4000_0000, ptr,
"Failed decoding RawRef: {bytes:?}"
);
Self::Value { hash }
}
}
Expand Down Expand Up @@ -426,7 +435,7 @@ impl<'a> TryFrom<&'a ProofNode> for Node<'a, Ref<'a>> {
/// which should be zero were not set to zero).
#[inline]
fn try_from(node: &'a ProofNode) -> Result<Self, Self::Error> {
decode_proof(node).ok_or(())
decode_proof(&*node.0).ok_or(())
}
}

Expand Down Expand Up @@ -647,15 +656,21 @@ fn decode_raw<'a>(node: &'a RawNode) -> Node<'a, RawRef<'a>> {
let (_, value) = stdx::split_array_ref::<4, 32, 36>(left);
let value_hash = value.into();
let child = RawNodeRef::try_from(RawRef::from_raw(right))
.map_err(|hash| debug_assert_eq!(CryptoHash::default(), *hash))
.map_err(|hash| {
debug_assert_eq!(
CryptoHash::default(),
*hash,
"Failed decoding raw node: {:?}",
node.0
)
})
.ok();
Node::Value { value_hash, child }
}
}

/// Decodes a node as represented in a proof.
fn decode_proof<'a>(node: &'a ProofNode) -> Option<Node<'a, Ref<'a>>> {
let bytes = &*node.0;
fn decode_proof<'a>(bytes: &'a [u8]) -> Option<Node<'a, Ref<'a>>> {
let (&first, rest) = bytes.split_first()?;
if first & !3 == 0 {
// In branch the first byte is 0b0000_00vv.
Expand Down Expand Up @@ -771,3 +786,101 @@ fn build_proof_extension(
dest[len..len + 32].copy_from_slice(child.hash.as_slice());
Some(len + 32)
}

// =============================================================================
// Debug

impl core::fmt::Debug for RawNode {
fn fmt(&self, fmtr: &mut core::fmt::Formatter) -> core::fmt::Result {
fn write_raw_key(
fmtr: &mut core::fmt::Formatter,
separator: &str,
bytes: &[u8; 36],
) -> core::fmt::Result {
let (tag, key) = stdx::split_array_ref::<2, 34, 36>(bytes);
write!(fmtr, "{separator}{:04x}", u16::from_be_bytes(*tag))?;
write_binary(fmtr, ":", key)
}

fn write_raw_ptr(
fmtr: &mut core::fmt::Formatter,
separator: &str,
bytes: &[u8; 36],
) -> core::fmt::Result {
let (ptr, hash) = stdx::split_array_ref::<4, 32, 36>(bytes);
let ptr = u32::from_be_bytes(*ptr);
let hash = <&CryptoHash>::from(hash);
write!(fmtr, "{separator}{ptr:08x}:{hash}")
}

let (left, right) = self.halfs();
if self.first() & 0xC0 == 0x80 {
write_raw_key(fmtr, "", left)
} else {
write_raw_ptr(fmtr, "", left)
}?;
write_raw_ptr(fmtr, ":", right)
}
}

impl core::fmt::Debug for ProofNode {
fn fmt(&self, fmtr: &mut core::fmt::Formatter) -> core::fmt::Result {
write_proof(fmtr, &self.0[..])
}
}

#[cfg(test)]
pub(crate) struct BorrowedProofNode<'a>(pub &'a [u8]);

#[cfg(test)]
impl core::fmt::Debug for BorrowedProofNode<'_> {
fn fmt(&self, fmtr: &mut core::fmt::Formatter) -> core::fmt::Result {
write_proof(fmtr, self.0)
}
}

fn write_proof(
fmtr: &mut core::fmt::Formatter,
bytes: &[u8],
) -> core::fmt::Result {
let first = match bytes.first() {
Some(byte) => *byte,
None => return fmtr.write_str("∅"),
};
let len = bytes.len();
if first & 0x80 == 0 && len == 65 {
let bytes = <&[u8; 64]>::try_from(&bytes[1..]).unwrap();
let (left, right) = stdx::split_array_ref::<32, 32, 64>(bytes);
let left = <&CryptoHash>::from(left);
let right = <&CryptoHash>::from(right);
write!(fmtr, "{first:02x}:{left}:{right}")
} else if first & 0xC0 == 0x80 && len >= 35 {
let (tag, bytes) = stdx::split_at::<2>(bytes).unwrap();
let (key, hash) = stdx::rsplit_at::<32>(bytes).unwrap();
write!(fmtr, "{:04x}", u16::from_be_bytes(*tag))?;
write_binary(fmtr, ":", key)?;
write!(fmtr, ":{}", <&CryptoHash>::from(hash))
} else if first & 0xC0 == 0xC0 && (len == 33 || len == 65) {
let (hash, rest) = stdx::split_at::<32>(&bytes[1..]).unwrap();
write!(fmtr, "{first:02x}:{}", <&CryptoHash>::from(hash))?;
if !rest.is_empty() {
let hash = <&[u8; 32]>::try_from(rest).unwrap();
write!(fmtr, "{}", <&CryptoHash>::from(hash))?;
}
Ok(())
} else {
write_binary(fmtr, "", bytes)
}
}

fn write_binary(
fmtr: &mut core::fmt::Formatter,
mut separator: &str,
bytes: &[u8],
) -> core::fmt::Result {
for byte in bytes {
write!(fmtr, "{separator}{byte:02x}")?;
separator = "_";
}
Ok(())
}
27 changes: 18 additions & 9 deletions sealable-trie/src/nodes/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ use alloc::boxed::Box;

use pretty_assertions::assert_eq;

use super::*;
use crate::bits;
use crate::hash::CryptoHash;
use crate::memory::Ptr;
use crate::nodes::{
Node, NodeRef, ProofNode, RawNode, RawNodeRef, RawRef, Ref,
};

const DEAD: Ptr = match Ptr::new(0xDEAD) {
Ok(Some(ptr)) => ptr,
Expand All @@ -24,8 +26,9 @@ const TWO: CryptoHash = CryptoHash([2; 32]);
/// Converts `Node` into `RawNode` and then back into `Node`. Panics if the
/// first and last objects aren’t equal. Returns the raw node.
#[track_caller]
fn raw_from_node(node: &Node) -> RawNode {
let raw = RawNode::try_from(node).unwrap();
pub(super) fn raw_from_node(node: &Node) -> RawNode {
let raw = RawNode::try_from(node)
.unwrap_or_else(|()| panic!("Failed encoding node as raw: {node:?}"));
let decoded = Node::from(&raw);
assert_eq!(
*node, decoded,
Expand All @@ -40,10 +43,13 @@ fn raw_from_node(node: &Node) -> RawNode {
/// `ProofNode` and then back into `Node`. Panics if the first and last
/// objects aren’t equal. Returns the proof node.
#[track_caller]
fn proof_from_node(node: &Node) -> ProofNode {
pub(super) fn proof_from_node(node: &Node) -> ProofNode {
let node = node.map_refs(Ref::from, NodeRef::from);
let proof = ProofNode::try_from(node).unwrap();
let decoded = Node::try_from(&proof).unwrap();
let proof = ProofNode::try_from(node)
.unwrap_or_else(|()| panic!("Failed encoding node as proof: {node:?}"));
let decoded = Node::try_from(&proof).unwrap_or_else(|()| {
panic!("Failed round-trip proof decoding of: {:?}", proof)
});
assert_eq!(
node, decoded,
"Node → ProofNode → Node gave different result:\n Proof: {proof:?}"
Expand All @@ -64,7 +70,7 @@ fn check_node_encoding(node: Node, want_raw: [u8; 72], want_proof: &[u8]) {
let raw = raw_from_node(&node);
assert_eq!(want_raw, raw.0, "Unexpected raw representation");
let proof = proof_from_node(&node);
assert_eq!(want_proof, &proof.0[..], "Unexpected proof representation");
assert_eq!(want_proof, &proof[..], "Unexpected proof representation");

assert_eq!(proof, ProofNode::from(raw), "Bad Raw → Proof conversion");

Expand All @@ -77,8 +83,11 @@ fn check_node_encoding(node: Node, want_raw: [u8; 72], want_proof: &[u8]) {

#[track_caller]
fn check_invalid_proof_node(bytes: &[u8]) {
let node = ProofNode(Box::from(bytes));
assert_eq!(Err(()), Node::try_from(&node));
assert_eq!(
Err(()),
Node::try_from(&ProofNode(Box::from(bytes))),
"Unexpectedly parsed invalid proof node: {bytes:x?}"
);
}

#[test]
Expand Down
Loading

0 comments on commit 55e3dc2

Please sign in to comment.