use alloy_primitives::{Address, B256, FixedBytes, Keccak256, Signature, address, b256, hex};
use alloy_signer::SignerSync;
use alloy_signer_local::PrivateKeySigner;
use bytes::{Bytes, BytesMut};
use std::fmt;
use std::marker::PhantomData;
use crate::PrimitivesError;
use crate::bmt::DEFAULT_BODY_SIZE;
use crate::cache::OnceCache;
use crate::chunk::error::{self, ChunkError};
use crate::error::Result;
use super::bmt_body::BmtBody;
use super::traits::{BmtChunk, Chunk, ChunkAddress, ChunkHeader, ChunkMetadata};
const ID_SIZE: usize = std::mem::size_of::<B256>();
const SIGNATURE_SIZE: usize = 65;
const MIN_SOC_FIELDS_SIZE: usize = ID_SIZE + SIGNATURE_SIZE;
const DISPERSED_REPLICA_OWNER: Address = address!("0xdc5b20847f43d67928f49cd4f85d696b5a7617b5");
const DISPERSED_REPLICA_OWNER_PK: B256 =
b256!("0x0100000000000000000000000000000000000000000000000000000000000000");
#[derive(Debug, Clone)]
pub struct SingleOwnerChunk<const BODY_SIZE: usize = DEFAULT_BODY_SIZE> {
header: SingleOwnerChunkHeader,
body: BmtBody<BODY_SIZE>,
chunk_address_cache: OnceCache<ChunkAddress>,
owner_cache: OnceCache<Address>,
}
#[derive(Debug, Clone)]
pub struct SingleOwnerChunkMetadata {
id: B256,
signature: Signature,
}
impl SingleOwnerChunkMetadata {
pub const fn new(id: B256, signature: Signature) -> Self {
Self { id, signature }
}
pub const fn id(&self) -> B256 {
self.id
}
pub const fn signature(&self) -> &Signature {
&self.signature
}
}
impl ChunkMetadata for SingleOwnerChunkMetadata {
fn bytes(&self) -> Bytes {
let mut bytes = BytesMut::with_capacity(ID_SIZE + SIGNATURE_SIZE);
bytes.extend_from_slice(self.id.as_ref());
bytes.extend_from_slice(&self.signature.as_bytes());
bytes.freeze()
}
}
#[derive(Debug, Clone)]
pub struct SingleOwnerChunkHeader {
metadata: SingleOwnerChunkMetadata,
}
impl SingleOwnerChunkHeader {
pub const fn new(metadata: SingleOwnerChunkMetadata) -> Self {
Self { metadata }
}
}
impl ChunkHeader for SingleOwnerChunkHeader {
type Metadata = SingleOwnerChunkMetadata;
fn id(&self) -> u8 {
1
}
fn version(&self) -> u8 {
1
}
fn metadata(&self) -> &Self::Metadata {
&self.metadata
}
fn bytes(&self) -> Bytes {
self.metadata.bytes()
}
}
impl<const BODY_SIZE: usize> SingleOwnerChunk<BODY_SIZE> {
#[must_use = "this returns a new chunk without modifying the input"]
pub fn new(id: B256, data: impl Into<Bytes>, signer: &impl SignerSync) -> Result<Self> {
SingleOwnerChunkBuilderImpl::<BODY_SIZE, Initial>::default()
.auto_from_data(data)?
.with_id(id)
.with_signer(signer)?
.build()
}
#[must_use = "this returns a new chunk without modifying the input"]
pub fn with_signature(id: B256, signature: Signature, data: impl Into<Bytes>) -> Result<Self> {
SingleOwnerChunkBuilderImpl::<BODY_SIZE, Initial>::default()
.auto_from_data(data)?
.with_id(id)
.with_signature(signature)?
.build()
}
#[must_use = "this returns a new chunk without modifying the input"]
pub fn new_dispersed_replica(mined_byte: u8, body: BmtBody<BODY_SIZE>) -> Result<Self> {
SingleOwnerChunkBuilderImpl::<BODY_SIZE, Initial>::default()
.with_body(body)
.dispersed_replica(mined_byte)?
.build()
}
#[must_use]
pub const fn from_parts(id: B256, signature: Signature, body: BmtBody<BODY_SIZE>) -> Self {
let metadata = SingleOwnerChunkMetadata::new(id, signature);
let header = SingleOwnerChunkHeader::new(metadata);
Self {
header,
body,
chunk_address_cache: OnceCache::new(),
owner_cache: OnceCache::new(),
}
}
#[must_use]
pub fn from_parts_with_caches(
id: B256,
signature: Signature,
body: BmtBody<BODY_SIZE>,
address: ChunkAddress,
owner: Address,
) -> Self {
let metadata = SingleOwnerChunkMetadata::new(id, signature);
let header = SingleOwnerChunkHeader::new(metadata);
Self {
header,
body,
chunk_address_cache: OnceCache::with_value(address),
owner_cache: OnceCache::with_value(owner),
}
}
pub fn owner(&self) -> error::Result<Address> {
if let Some(addr) = self.owner_cache.get() {
return Ok(*addr);
}
let addr = self.calculate_owner()?;
let _ = self.owner_cache.try_set(addr);
Ok(addr)
}
fn calculate_owner(&self) -> error::Result<Address> {
let hash = Self::to_sign(&self.header.metadata.id, &self.body);
self.signature()
.recover_address_from_msg(hash)
.map_err(Into::into)
}
fn to_sign(id: &B256, body: &BmtBody<BODY_SIZE>) -> B256 {
let mut hasher = Keccak256::new();
hasher.update(id);
hasher.update(body.hash());
hasher.finalize()
}
fn is_valid_replica(&self) -> bool {
self.id()[1..] == self.body.hash().as_slice()[1..]
}
pub const fn id(&self) -> B256 {
self.header.metadata.id
}
pub const fn signature(&self) -> &Signature {
&self.header.metadata.signature
}
}
impl<const BODY_SIZE: usize> Chunk for SingleOwnerChunk<BODY_SIZE> {
type Header = SingleOwnerChunkHeader;
fn address(&self) -> &ChunkAddress {
self.chunk_address_cache.get_or_compute(|| {
let owner = self.owner().unwrap_or(Address::ZERO);
let mut hasher = Keccak256::new();
hasher.update(self.id());
hasher.update(owner);
hasher.finalize().into()
})
}
fn data(&self) -> &Bytes {
self.body.data()
}
fn size(&self) -> usize {
self.header().bytes().len() + self.body.size()
}
fn header(&self) -> &Self::Header {
&self.header
}
fn verify(&self, expected: &ChunkAddress) -> Result<()> {
let actual = self.address();
let owner = self.owner()?;
if owner == DISPERSED_REPLICA_OWNER && !self.is_valid_replica() {
return Err(error::ChunkError::invalid_format("invalid dispersed replica").into());
}
if actual != expected {
return Err(error::ChunkError::verification_failed(*expected, *actual).into());
}
Ok(())
}
}
impl<const BODY_SIZE: usize> BmtChunk for SingleOwnerChunk<BODY_SIZE> {
fn span(&self) -> u64 {
self.body.span()
}
}
impl<const BODY_SIZE: usize> From<SingleOwnerChunk<BODY_SIZE>> for Bytes {
fn from(chunk: SingleOwnerChunk<BODY_SIZE>) -> Self {
let mut bytes = BytesMut::with_capacity(chunk.size());
bytes.extend_from_slice(chunk.header().bytes().as_ref());
bytes.extend_from_slice(&Self::from(chunk.body));
bytes.freeze()
}
}
impl<const BODY_SIZE: usize> TryFrom<Bytes> for SingleOwnerChunk<BODY_SIZE> {
type Error = PrimitivesError;
fn try_from(bytes: Bytes) -> Result<Self> {
if bytes.len() < MIN_SOC_FIELDS_SIZE {
return Err(ChunkError::invalid_size(
"insufficient data for single-owner chunk",
MIN_SOC_FIELDS_SIZE,
bytes.len(),
)
.into());
}
let id_slice = &bytes.slice(0..ID_SIZE);
let mut id = FixedBytes::<32>::default();
id.copy_from_slice(id_slice);
let sig_slice = &bytes.slice(ID_SIZE..ID_SIZE + SIGNATURE_SIZE);
let signature = Signature::from_raw(sig_slice).map_err(ChunkError::from)?;
let body_bytes = bytes.slice(ID_SIZE + SIGNATURE_SIZE..);
let body = BmtBody::try_from(body_bytes)?;
let metadata = SingleOwnerChunkMetadata::new(id, signature);
let header = SingleOwnerChunkHeader::new(metadata);
Ok(Self {
header,
body,
chunk_address_cache: OnceCache::new(),
owner_cache: OnceCache::new(),
})
}
}
impl<const BODY_SIZE: usize> TryFrom<&[u8]> for SingleOwnerChunk<BODY_SIZE> {
type Error = PrimitivesError;
fn try_from(bytes: &[u8]) -> Result<Self> {
Self::try_from(Bytes::copy_from_slice(bytes))
}
}
impl<const BODY_SIZE: usize> fmt::Display for SingleOwnerChunk<BODY_SIZE> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let owner_str = self.owner().map_or_else(
|_| "invalid".to_string(),
|addr| hex::encode(addr.as_slice()),
);
write!(
f,
"SingleOwnerChunk[id={}, owner={}]",
hex::encode(&self.id()[..8]),
owner_str
)
}
}
impl<const BODY_SIZE: usize> PartialEq for SingleOwnerChunk<BODY_SIZE> {
fn eq(&self, other: &Self) -> bool {
match (self.owner(), other.owner()) {
(Ok(a), Ok(b)) => self.id() == other.id() && a == b,
_ => false,
}
}
}
impl<const BODY_SIZE: usize> Eq for SingleOwnerChunk<BODY_SIZE> {}
impl<const BODY_SIZE: usize> super::chunk_type::ChunkType for SingleOwnerChunk<BODY_SIZE> {
const TYPE_ID: super::type_id::ChunkTypeId = super::type_id::ChunkTypeId::SINGLE_OWNER;
const TYPE_NAME: &'static str = "single_owner";
}
trait BuilderState {}
#[derive(Debug, Default)]
struct Initial;
impl BuilderState for Initial {}
#[derive(Debug)]
struct WithData;
impl BuilderState for WithData {}
#[derive(Debug)]
struct WithId;
impl BuilderState for WithId {}
#[derive(Debug)]
struct ReadyToBuild;
impl BuilderState for ReadyToBuild {}
#[derive(Debug)]
struct SingleOwnerChunkBuilderImpl<const BODY_SIZE: usize, S: BuilderState = Initial> {
body: Option<BmtBody<BODY_SIZE>>,
id: Option<B256>,
signature: Option<Signature>,
_state: PhantomData<S>,
}
impl<const BODY_SIZE: usize> Default for SingleOwnerChunkBuilderImpl<BODY_SIZE, Initial> {
fn default() -> Self {
Self {
body: None,
id: None,
signature: None,
_state: PhantomData,
}
}
}
impl<const BODY_SIZE: usize> SingleOwnerChunkBuilderImpl<BODY_SIZE, Initial> {
fn auto_from_data(
mut self,
data: impl Into<Bytes>,
) -> Result<SingleOwnerChunkBuilderImpl<BODY_SIZE, WithData>> {
let body = BmtBody::<BODY_SIZE>::builder()
.auto_from_data(data)?
.build()?;
self.body = Some(body);
Ok(SingleOwnerChunkBuilderImpl {
body: self.body,
id: self.id,
signature: self.signature,
_state: PhantomData,
})
}
fn with_body(
mut self,
body: BmtBody<BODY_SIZE>,
) -> SingleOwnerChunkBuilderImpl<BODY_SIZE, WithData> {
self.body = Some(body);
SingleOwnerChunkBuilderImpl {
body: self.body,
id: self.id,
signature: self.signature,
_state: PhantomData,
}
}
}
impl<const BODY_SIZE: usize> SingleOwnerChunkBuilderImpl<BODY_SIZE, WithData> {
fn with_id(mut self, id: B256) -> SingleOwnerChunkBuilderImpl<BODY_SIZE, WithId> {
self.id = Some(id);
SingleOwnerChunkBuilderImpl {
body: self.body,
id: self.id,
signature: self.signature,
_state: PhantomData,
}
}
fn dispersed_replica(
self,
first_byte: u8,
) -> Result<SingleOwnerChunkBuilderImpl<BODY_SIZE, ReadyToBuild>> {
let body_hash = self.body.as_ref().unwrap().hash();
let mut id = B256::default();
id[0] = first_byte;
id[1..].copy_from_slice(&body_hash.as_slice()[1..]);
let signer = PrivateKeySigner::from_slice(DISPERSED_REPLICA_OWNER_PK.as_slice()).unwrap();
self.with_id(id).with_signer(&signer)
}
}
impl<const BODY_SIZE: usize> SingleOwnerChunkBuilderImpl<BODY_SIZE, WithId> {
fn with_signer(
self,
signer: &impl SignerSync,
) -> Result<SingleOwnerChunkBuilderImpl<BODY_SIZE, ReadyToBuild>> {
let body = self.body.as_ref().unwrap();
let id = self.id.as_ref().unwrap();
let hash = SingleOwnerChunk::<BODY_SIZE>::to_sign(id, body);
let signature = signer
.sign_message_sync(hash.as_ref())
.map_err(ChunkError::from)?;
self.with_signature(signature)
}
fn with_signature(
mut self,
signature: Signature,
) -> Result<SingleOwnerChunkBuilderImpl<BODY_SIZE, ReadyToBuild>> {
self.signature = Some(signature);
Ok(SingleOwnerChunkBuilderImpl {
body: self.body,
id: self.id,
signature: self.signature,
_state: PhantomData,
})
}
}
impl<const BODY_SIZE: usize> SingleOwnerChunkBuilderImpl<BODY_SIZE, ReadyToBuild> {
fn build(self) -> Result<SingleOwnerChunk<BODY_SIZE>> {
let body = self.body.unwrap();
let id = self.id.unwrap();
let signature = self.signature.unwrap();
Ok(SingleOwnerChunk::from_parts(id, signature, body))
}
}
#[cfg(any(test, feature = "arbitrary"))]
impl<'a, const BODY_SIZE: usize> arbitrary::Arbitrary<'a> for SingleOwnerChunk<BODY_SIZE> {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let id = B256::arbitrary(u)?;
let body = BmtBody::<BODY_SIZE>::arbitrary(u)?;
let signer = alloy_signer_local::PrivateKeySigner::random();
Ok(SingleOwnerChunkBuilderImpl::<BODY_SIZE, Initial>::default()
.with_body(body)
.with_id(id)
.with_signer(&signer)
.unwrap()
.build()
.unwrap())
}
}
#[cfg(test)]
mod tests {
use crate::DEFAULT_BODY_SIZE;
use super::*;
use alloy_primitives::hex;
use proptest::prelude::*;
use proptest_arbitrary_interop::arb;
type DefaultSingleOwnerChunk = SingleOwnerChunk<DEFAULT_BODY_SIZE>;
fn get_test_wallet() -> PrivateKeySigner {
let pk = hex!("2c7536e3605d9c16a7a3d7b1898e529396a65c23a3bcbd4012a11cf2731b0fbc");
PrivateKeySigner::from_slice(&pk).unwrap()
}
fn chunk_strategy() -> impl Strategy<Value = DefaultSingleOwnerChunk> {
arb::<DefaultSingleOwnerChunk>()
}
proptest! {
#[test]
fn test_chunk_properties(chunk in chunk_strategy()) {
prop_assert!(chunk.size() >= MIN_SOC_FIELDS_SIZE);
let bytes: Bytes = chunk.clone().into();
let decoded = DefaultSingleOwnerChunk::try_from(bytes.as_ref()).unwrap();
prop_assert_eq!(chunk.id(), decoded.id());
prop_assert_eq!(chunk.signature(), decoded.signature());
prop_assert_eq!(chunk.data(), decoded.data());
prop_assert_eq!(chunk.owner().unwrap(), decoded.owner().unwrap());
let address = chunk.address();
prop_assert!(chunk.verify(address).is_ok());
}
#[test]
fn test_dispersed_replica_properties(first_byte in any::<u8>(), data in proptest::collection::vec(any::<u8>(), 1..DEFAULT_BODY_SIZE)) {
let chunk = DefaultSingleOwnerChunk::new_dispersed_replica(first_byte, BmtBody::<DEFAULT_BODY_SIZE>::builder().auto_from_data(data).unwrap().build().unwrap()).unwrap();
prop_assert!(chunk.is_valid_replica());
prop_assert_eq!(chunk.id()[0], first_byte);
prop_assert_eq!(chunk.owner().unwrap(), DISPERSED_REPLICA_OWNER);
prop_assert!(chunk.verify(chunk.address()).is_ok());
}
#[test]
fn test_chunk_creation(id in arb::<B256>(), data in proptest::collection::vec(any::<u8>(), 1..DEFAULT_BODY_SIZE)) {
let wallet = get_test_wallet();
let chunk = SingleOwnerChunkBuilderImpl::<DEFAULT_BODY_SIZE, Initial>::default()
.with_body(
BmtBody::<DEFAULT_BODY_SIZE>::builder()
.auto_from_data(data.clone())
.unwrap()
.build()
.unwrap(),
)
.with_id(id)
.with_signer(&wallet)
.unwrap()
.build()
.unwrap();
prop_assert_eq!(chunk.id(), id);
prop_assert_eq!(chunk.data(), &data);
prop_assert!(!chunk.owner().unwrap().is_zero());
}
#[test]
fn test_dispersed_replica_mismatched_address(first_byte in any::<u8>(), data in proptest::collection::vec(any::<u8>(), 1..DEFAULT_BODY_SIZE)) {
let chunk = SingleOwnerChunkBuilderImpl::<DEFAULT_BODY_SIZE, Initial>::default().with_body(
BmtBody::<DEFAULT_BODY_SIZE>::builder()
.auto_from_data(data)
.unwrap()
.build()
.unwrap(),
).dispersed_replica(first_byte).unwrap().build().unwrap();
let replica_address = *chunk.address();
let bytes: Bytes = chunk.into();
let mut modified_bytes = bytes.to_vec();
modified_bytes[1..ID_SIZE].copy_from_slice(&[0x01; 31]);
let modified_chunk = DefaultSingleOwnerChunk::try_from(modified_bytes.as_slice()).unwrap();
prop_assert!(!modified_chunk.is_valid_replica());
prop_assert!(modified_chunk.verify(&replica_address).is_err());
}
#[test]
fn test_chunk_invalid_signature(id in arb::<B256>(), data in proptest::collection::vec(any::<u8>(), 1..DEFAULT_BODY_SIZE)) {
let wallet = get_test_wallet();
let chunk = DefaultSingleOwnerChunk::new(id, data, &wallet).unwrap();
let original_address = *chunk.address();
let bytes: Bytes = chunk.into();
let mut modified_bytes = bytes.to_vec();
modified_bytes[ID_SIZE..ID_SIZE + 65].copy_from_slice(&[0xff; 65]);
let modified_chunk = DefaultSingleOwnerChunk::try_from(modified_bytes.as_slice()).unwrap();
prop_assert!(modified_chunk.verify(&original_address).is_err());
prop_assert!(modified_chunk.owner().is_err());
}
#[test]
fn test_chunk_too_small(data in proptest::collection::vec(any::<u8>(), 1..MIN_SOC_FIELDS_SIZE)) {
let chunk = DefaultSingleOwnerChunk::try_from(data.as_slice());
prop_assert!(chunk.is_err());
}
}
#[test]
fn test_new() {
let id = B256::ZERO;
let data = b"foo".to_vec();
let wallet = get_test_wallet();
let chunk = DefaultSingleOwnerChunk::new(id, data.clone(), &wallet).unwrap();
assert_eq!(chunk.id(), id);
assert_eq!(chunk.data(), &data);
}
#[test]
fn test_new_signed() {
let id = B256::ZERO;
let data = b"foo".to_vec();
let sig = hex!(
"5acd384febc133b7b245e5ddc62d82d2cded9182d2716126cd8844509af65a053deb418208027f548e3e88343af6f84a8772fb3cebc0a1833a0ea7ec0c1348311b"
);
let signature = Signature::try_from(sig.as_slice()).unwrap();
let chunk = SingleOwnerChunkBuilderImpl::<DEFAULT_BODY_SIZE, Initial>::default()
.auto_from_data(data.clone())
.unwrap()
.with_id(id)
.with_signature(signature)
.unwrap()
.build()
.unwrap();
assert_eq!(chunk.id(), id);
assert_eq!(chunk.data(), &data);
assert_eq!(chunk.signature().as_bytes(), sig);
let expected_owner = address!("8d3766440f0d7b949a5e32995d09619a7f86e632");
assert_eq!(chunk.owner().unwrap(), expected_owner);
}
fn get_test_chunk_data() -> Vec<u8> {
hex!(
"000000000000000000000000000000000000000000000000000000000000000\
05acd384febc133b7b245e5ddc62d82d2cded9182d2716126cd8844509af65a05\
3deb418208027f548e3e88343af6f84a8772fb3cebc0a1833a0ea7ec0c134831\
1b0300000000000000666f6f"
)
.to_vec()
}
#[test]
fn test_chunk_address() {
let chunk = DefaultSingleOwnerChunk::try_from(get_test_chunk_data().as_slice()).unwrap();
let expected_owner = address!("8d3766440f0d7b949a5e32995d09619a7f86e632");
assert_eq!(chunk.owner().unwrap(), expected_owner);
let expected_address =
b256!("9d453ebb73b2fedaaf44ceddcf7a0aa37f3e3d6453fea5841c31f0ea6d61dc85");
assert_eq!(chunk.address().as_ref(), expected_address);
}
#[test]
fn test_invalid_dispersed_replica() -> Result<()> {
let test_data = b"test data".to_vec();
let dispersed_replica_wallet =
PrivateKeySigner::from_slice(DISPERSED_REPLICA_OWNER_PK.as_slice()).unwrap();
let chunk = SingleOwnerChunkBuilderImpl::<DEFAULT_BODY_SIZE, Initial>::default()
.with_body(
BmtBody::<DEFAULT_BODY_SIZE>::builder()
.auto_from_data(test_data)?
.build()?,
)
.with_id(B256::ZERO)
.with_signer(&dispersed_replica_wallet)?
.build()?;
let replica_address = chunk.address();
assert!(!chunk.is_valid_replica());
assert!(matches!(
chunk.verify(replica_address),
Err(PrimitivesError::Chunk(ChunkError::InvalidFormat { .. }))
));
Ok(())
}
}