diff --git a/libstackerdb/src/tests/mod.rs b/libstackerdb/src/tests/mod.rs index 4327eb2fb05..6edbb79f945 100644 --- a/libstackerdb/src/tests/mod.rs +++ b/libstackerdb/src/tests/mod.rs @@ -23,6 +23,24 @@ use stacks_common::util::secp256k1::MessageSignature; use crate::*; +#[test] +fn test_stackerdb_chunk_data_consensus_codec_round_trip() { + let original = StackerDBChunkData { + slot_id: 0x0a0b0c0d, + slot_version: 0x11223344, + sig: MessageSignature([0xaa; 65]), + data: vec![0xde, 0xad, 0xbe, 0xef, 0x00, 0xff], + }; + let bytes = original.serialize_to_vec(); + // Empty / no-op serialize would leave bytes empty; the underflow check below traps that. + assert!(!bytes.is_empty(), "serialize must write bytes"); + let decoded = StackerDBChunkData::consensus_deserialize(&mut &bytes[..]).unwrap(); + assert_eq!(decoded.slot_id, original.slot_id); + assert_eq!(decoded.slot_version, original.slot_version); + assert_eq!(decoded.sig.0, original.sig.0); + assert_eq!(decoded.data, original.data); +} + #[test] fn test_stackerdb_slot_metadata_sign_verify() { let pk = StacksPrivateKey::random(); @@ -67,6 +85,27 @@ fn test_stackerdb_slot_metadata_sign_verify() { assert!(!bad_slot_metadata.verify(&addr).unwrap()); } +#[test] +fn test_stackerdb_chunk_data_sign_verify() { + let pk = StacksPrivateKey::random(); + let addr = StacksAddress::from_public_keys( + C32_ADDRESS_VERSION_MAINNET_SINGLESIG, + &AddressHashMode::SerializeP2PKH, + 1, + &vec![StacksPublicKey::from_private(&pk)], + ) + .unwrap(); + let bad_addr = StacksAddress::new(0x01, Hash160([0x01; 20])).unwrap(); + + let mut chunk_data = StackerDBChunkData::new(7, 3, vec![0xab; 64]); + chunk_data.sign(&pk).unwrap(); + + // The chunk verifies against the address whose private key signed it. + assert!(chunk_data.verify(&addr).unwrap()); + // The chunk does not verify against an unrelated address. + assert!(!chunk_data.verify(&bad_addr).unwrap()); +} + #[test] fn test_stackerdb_paths() { let pk = StacksPrivateKey::from_hex( diff --git a/stacks-codec/src/lib.rs b/stacks-codec/src/lib.rs index dbdf88400da..05df02698a9 100644 --- a/stacks-codec/src/lib.rs +++ b/stacks-codec/src/lib.rs @@ -266,3 +266,54 @@ pub const PEER_ADDRESS_ENCODED_SIZE: u32 = 16; pub const HASH160_ENCODED_SIZE: u32 = 20; pub const MESSAGE_SIGNATURE_ENCODED_SIZE: u32 = 65; + +#[cfg(test)] +mod tests { + use crate::*; + + fn encode_vec_u32_len_prefix(len: u32, items: &[u32]) -> Vec { + let mut buf = Vec::with_capacity(4 + items.len() * 4); + buf.extend_from_slice(&len.to_be_bytes()); + for x in items { + buf.extend_from_slice(&x.to_be_bytes()); + } + buf + } + + #[test] + fn read_next_at_most_enforces_length_bound() { + // len < max: succeeds with the decoded items. + let bytes = encode_vec_u32_len_prefix(3, &[10, 20, 30]); + let v: Vec = read_next_at_most(&mut &bytes[..], 5).unwrap(); + assert_eq!(v, vec![10, 20, 30]); + + // len == max: succeeds (the bound is inclusive: `len > max` is what rejects). + let bytes = encode_vec_u32_len_prefix(5, &[1, 2, 3, 4, 5]); + let v: Vec = read_next_at_most(&mut &bytes[..], 5).unwrap(); + assert_eq!(v, vec![1, 2, 3, 4, 5]); + + // len > max: errors with DeserializeError. + let bytes = encode_vec_u32_len_prefix(6, &[1, 2, 3, 4, 5, 6]); + let r: Result, Error> = read_next_at_most(&mut &bytes[..], 5); + assert!( + matches!(r, Err(Error::DeserializeError(_))), + "expected DeserializeError, got {r:?}" + ); + } + + #[test] + fn read_next_exact_enforces_exact_length() { + // len == num_items: succeeds. + let bytes = encode_vec_u32_len_prefix(5, &[1, 2, 3, 4, 5]); + let v: Vec = read_next_exact(&mut &bytes[..], 5).unwrap(); + assert_eq!(v, vec![1, 2, 3, 4, 5]); + + // len != num_items: errors. + let bytes = encode_vec_u32_len_prefix(3, &[10, 20, 30]); + let r: Result, Error> = read_next_exact(&mut &bytes[..], 5); + assert!( + matches!(r, Err(Error::DeserializeError(_))), + "expected DeserializeError, got {r:?}" + ); + } +}