use alloy_primitives::{Address, B256, Signature, eip191_hash_message};
use alloy_signer::k256::ecdsa::VerifyingKey;
use byteorder::{BigEndian, ByteOrder};
use nectar_primitives::SwarmAddress;
use crate::{BatchId, StampError};
pub const STAMP_SIZE: usize = 113;
pub type StampBytes = [u8; STAMP_SIZE];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct StampIndex {
bucket: u32,
index: u32,
}
impl StampIndex {
#[inline]
pub const fn new(bucket: u32, index: u32) -> Self {
Self { bucket, index }
}
#[inline]
pub const fn bucket(&self) -> u32 {
self.bucket
}
#[inline]
pub const fn index(&self) -> u32 {
self.index
}
#[inline]
pub const fn encode(&self) -> u64 {
((self.bucket as u64) << 32) | (self.index as u64)
}
#[inline]
pub const fn decode(encoded: u64) -> Self {
Self {
bucket: (encoded >> 32) as u32,
index: encoded as u32,
}
}
#[inline]
pub const fn to_be_bytes(&self) -> [u8; 8] {
self.encode().to_be_bytes()
}
#[inline]
pub const fn from_be_bytes(bytes: [u8; 8]) -> Self {
Self::decode(u64::from_be_bytes(bytes))
}
}
impl From<(u32, u32)> for StampIndex {
fn from((bucket, index): (u32, u32)) -> Self {
Self::new(bucket, index)
}
}
impl From<StampIndex> for (u32, u32) {
fn from(idx: StampIndex) -> Self {
(idx.bucket, idx.index)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Stamp {
batch: BatchId,
index: StampIndex,
timestamp: u64,
sig: Signature,
}
impl Stamp {
#[inline]
pub const fn new(
batch: BatchId,
bucket: u32,
index: u32,
timestamp: u64,
sig: Signature,
) -> Self {
Self {
batch,
index: StampIndex::new(bucket, index),
timestamp,
sig,
}
}
#[inline]
pub const fn with_index(
batch: BatchId,
index: StampIndex,
timestamp: u64,
sig: Signature,
) -> Self {
Self {
batch,
index,
timestamp,
sig,
}
}
#[inline]
pub const fn batch(&self) -> BatchId {
self.batch
}
#[inline]
pub const fn stamp_index(&self) -> StampIndex {
self.index
}
#[inline]
pub const fn bucket(&self) -> u32 {
self.index.bucket()
}
#[inline]
pub const fn index(&self) -> u32 {
self.index.index()
}
#[inline]
pub const fn timestamp(&self) -> u64 {
self.timestamp
}
#[inline]
pub const fn signature(&self) -> &Signature {
&self.sig
}
#[inline]
pub fn to_bytes(&self) -> StampBytes {
let mut bytes = [0u8; STAMP_SIZE];
bytes[..32].copy_from_slice(self.batch.as_slice());
BigEndian::write_u32(&mut bytes[32..36], self.index.bucket());
BigEndian::write_u32(&mut bytes[36..40], self.index.index());
BigEndian::write_u64(&mut bytes[40..48], self.timestamp);
bytes[48..STAMP_SIZE].copy_from_slice(&self.sig.as_bytes());
bytes
}
#[inline]
pub fn from_bytes(bytes: &StampBytes) -> Result<Self, StampError> {
let batch = B256::from_slice(&bytes[..32]);
let bucket = BigEndian::read_u32(&bytes[32..36]);
let index = BigEndian::read_u32(&bytes[36..40]);
let timestamp = BigEndian::read_u64(&bytes[40..48]);
let sig = Signature::from_raw(&bytes[48..STAMP_SIZE])
.map_err(|_| StampError::InvalidSignature)?;
Ok(Self {
batch,
index: StampIndex::new(bucket, index),
timestamp,
sig,
})
}
#[inline]
pub fn try_from_slice(bytes: &[u8]) -> Result<Self, StampError> {
if bytes.len() != STAMP_SIZE {
return Err(StampError::InvalidData("stamp must be exactly 113 bytes"));
}
let mut stamp_bytes = [0u8; STAMP_SIZE];
stamp_bytes.copy_from_slice(bytes);
Self::from_bytes(&stamp_bytes)
}
pub fn recover_signer(&self, chunk_address: &SwarmAddress) -> Result<Address, StampError> {
let digest = StampDigest::new(*chunk_address, self.batch, self.index, self.timestamp);
let prehash = digest.to_prehash();
self.sig
.recover_address_from_msg(prehash.as_slice())
.map_err(|_| StampError::InvalidSignature)
}
pub fn verify(&self, chunk_address: &SwarmAddress, owner: Address) -> Result<(), StampError> {
let recovered = self.recover_signer(chunk_address)?;
if recovered != owner {
return Err(StampError::OwnerMismatch {
expected: owner,
actual: recovered,
});
}
Ok(())
}
pub fn recover_pubkey(&self, chunk_address: &SwarmAddress) -> Result<VerifyingKey, StampError> {
let digest = StampDigest::new(*chunk_address, self.batch, self.index, self.timestamp);
let prehash = digest.to_prehash();
let msg_hash = eip191_hash_message(prehash.as_slice());
let k256_sig = self
.sig
.to_k256()
.map_err(|_| StampError::InvalidSignature)?;
let recovery_id = self.sig.recid();
VerifyingKey::recover_from_prehash(msg_hash.as_slice(), &k256_sig, recovery_id)
.map_err(|_| StampError::InvalidSignature)
}
pub fn verify_with_pubkey(
&self,
chunk_address: &SwarmAddress,
pubkey: &VerifyingKey,
) -> Result<(), StampError> {
use alloy_signer::k256::ecdsa::signature::hazmat::PrehashVerifier;
let digest = StampDigest::new(*chunk_address, self.batch, self.index, self.timestamp);
let prehash = digest.to_prehash();
let msg_hash = eip191_hash_message(prehash.as_slice());
let k256_sig = self
.sig
.to_k256()
.map_err(|_| StampError::InvalidSignature)?;
pubkey
.verify_prehash(msg_hash.as_slice(), &k256_sig)
.map_err(|_| StampError::InvalidSignature)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StampDigest {
pub chunk_address: SwarmAddress,
pub batch_id: BatchId,
pub index: StampIndex,
pub timestamp: u64,
}
impl StampDigest {
#[inline]
pub const fn new(
chunk_address: SwarmAddress,
batch_id: BatchId,
index: StampIndex,
timestamp: u64,
) -> Self {
Self {
chunk_address,
batch_id,
index,
timestamp,
}
}
pub fn to_prehash(&self) -> B256 {
use alloy_primitives::keccak256;
let mut data = [0u8; 32 + 32 + 8 + 8]; data[..32].copy_from_slice(self.chunk_address.as_bytes());
data[32..64].copy_from_slice(self.batch_id.as_slice());
data[64..72].copy_from_slice(&self.index.to_be_bytes());
data[72..80].copy_from_slice(&self.timestamp.to_be_bytes());
keccak256(data)
}
}
impl From<Stamp> for StampBytes {
#[inline]
fn from(stamp: Stamp) -> Self {
stamp.to_bytes()
}
}
impl TryFrom<StampBytes> for Stamp {
type Error = StampError;
#[inline]
fn try_from(bytes: StampBytes) -> Result<Self, Self::Error> {
Self::from_bytes(&bytes)
}
}
#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for StampIndex {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
Ok(Self::new(u.arbitrary()?, u.arbitrary()?))
}
}
#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for Stamp {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
use alloy_primitives::U256;
let batch: B256 = u.arbitrary()?;
let index = StampIndex::arbitrary(u)?;
let timestamp: u64 = u.arbitrary()?;
let r = U256::from_be_bytes(u.arbitrary::<[u8; 32]>()?);
let s = U256::from_be_bytes(u.arbitrary::<[u8; 32]>()?);
let v: bool = u.arbitrary()?;
let sig = Signature::new(r, s, v);
Ok(Self::with_index(batch, index, timestamp, sig))
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::hex;
const TEST_BATCH_ID: &str = "c3387832bb1b88acbcd0ffdb65a08ef077d98c08d4bee576a72dbe3d36761369";
const TEST_STAMP: &str = "c3387832bb1b88acbcd0ffdb65a08ef077d98c08d4bee576a72dbe3d367613690000cbe5000000000000018921ff0dbb29169df9e6364e26c6ca6b17745c10b9d6a36ea38e204f2e3cc64a8373c0661f5bb0a347c61d8d1689b0dcf8354117686a6a18d08cff927f526de5fc61b2b7491b";
#[test]
fn test_stamp_index_encode_decode() {
let idx = StampIndex::new(0x1234, 0x5678);
assert_eq!(idx.encode(), 0x0000123400005678);
let decoded = StampIndex::decode(0x0000123400005678);
assert_eq!(decoded, idx);
}
#[test]
fn test_stamp_index_bytes() {
let idx = StampIndex::new(0x1234, 0x5678);
let bytes = idx.to_be_bytes();
let restored = StampIndex::from_be_bytes(bytes);
assert_eq!(idx, restored);
}
#[test]
fn test_stamp_index_conversions() {
let idx = StampIndex::new(100, 50);
let tuple: (u32, u32) = idx.into();
assert_eq!(tuple, (100, 50));
let back: StampIndex = tuple.into();
assert_eq!(back, idx);
}
#[test]
fn test_stamp_roundtrip() {
let batch = B256::ZERO;
let sig = Signature::test_signature();
let stamp = Stamp::new(batch, 100, 50, 1234567890, sig);
let bytes = stamp.to_bytes();
let restored = Stamp::from_bytes(&bytes).unwrap();
assert_eq!(stamp, restored);
}
#[test]
fn test_stamp_from_known_data() {
let bytes = hex::decode(TEST_STAMP).unwrap();
let stamp = Stamp::try_from_slice(&bytes).unwrap();
let expected_batch = B256::from_slice(&hex::decode(TEST_BATCH_ID).unwrap());
assert_eq!(stamp.batch(), expected_batch);
assert_eq!(stamp.bucket(), 52197); assert_eq!(stamp.index(), 0);
assert_eq!(stamp.timestamp(), 1688492510651);
}
#[test]
fn test_stamp_with_index() {
let batch = B256::ZERO;
let idx = StampIndex::new(100, 50);
let sig = Signature::test_signature();
let stamp = Stamp::with_index(batch, idx, 1234567890, sig);
assert_eq!(stamp.stamp_index(), idx);
assert_eq!(stamp.bucket(), 100);
assert_eq!(stamp.index(), 50);
}
#[test]
fn test_stamp_size() {
assert_eq!(STAMP_SIZE, 113);
}
#[test]
fn test_invalid_slice_size() {
let bytes = [0u8; 100];
let result = Stamp::try_from_slice(&bytes);
assert!(matches!(result, Err(StampError::InvalidData(_))));
}
#[test]
fn test_from_conversions() {
let sig = Signature::test_signature();
let stamp = Stamp::new(B256::ZERO, 1, 2, 3, sig);
let bytes: StampBytes = stamp.clone().into();
let back: Stamp = bytes.try_into().unwrap();
assert_eq!(stamp, back);
}
#[test]
fn test_recover_signer() {
let chunk_addr_bytes =
hex::decode("0000000000000000000000000000000000000000000000000000000000000002")
.unwrap();
let full_stamp_bytes = hex::decode(
"000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000003496cb9ac06221d39c3f6a7dd3b9c2301c1f923162b90d5443e42023f34ff908945b0da1c297190f111b7c6ebc828648ead8f7fce06c0364cb5a833410230c5c01c"
).unwrap();
let expected_owner: Address = "8d3766440f0d7b949a5e32995d09619a7f86e632".parse().unwrap();
let chunk_address = SwarmAddress::new(chunk_addr_bytes.try_into().unwrap());
let stamp = Stamp::try_from_slice(&full_stamp_bytes).unwrap();
let recovered = stamp.recover_signer(&chunk_address).unwrap();
assert_eq!(recovered, expected_owner);
}
#[test]
fn test_verify() {
let chunk_addr_bytes =
hex::decode("0000000000000000000000000000000000000000000000000000000000000002")
.unwrap();
let full_stamp_bytes = hex::decode(
"000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000003496cb9ac06221d39c3f6a7dd3b9c2301c1f923162b90d5443e42023f34ff908945b0da1c297190f111b7c6ebc828648ead8f7fce06c0364cb5a833410230c5c01c"
).unwrap();
let expected_owner: Address = "8d3766440f0d7b949a5e32995d09619a7f86e632".parse().unwrap();
let wrong_owner: Address = "0000000000000000000000000000000000000001".parse().unwrap();
let chunk_address = SwarmAddress::new(chunk_addr_bytes.try_into().unwrap());
let stamp = Stamp::try_from_slice(&full_stamp_bytes).unwrap();
assert!(stamp.verify(&chunk_address, expected_owner).is_ok());
let result = stamp.verify(&chunk_address, wrong_owner);
assert!(matches!(result, Err(StampError::OwnerMismatch { .. })));
}
#[test]
fn test_recover_pubkey() {
use alloy_signer::utils::public_key_to_address;
let chunk_addr_bytes =
hex::decode("0000000000000000000000000000000000000000000000000000000000000002")
.unwrap();
let full_stamp_bytes = hex::decode(
"000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000003496cb9ac06221d39c3f6a7dd3b9c2301c1f923162b90d5443e42023f34ff908945b0da1c297190f111b7c6ebc828648ead8f7fce06c0364cb5a833410230c5c01c"
).unwrap();
let expected_owner: Address = "8d3766440f0d7b949a5e32995d09619a7f86e632".parse().unwrap();
let chunk_address = SwarmAddress::new(chunk_addr_bytes.try_into().unwrap());
let stamp = Stamp::try_from_slice(&full_stamp_bytes).unwrap();
let pubkey = stamp.recover_pubkey(&chunk_address).unwrap();
let recovered_addr = public_key_to_address(&pubkey);
assert_eq!(recovered_addr, expected_owner);
}
#[test]
fn test_verify_with_pubkey() {
let chunk_addr_bytes =
hex::decode("0000000000000000000000000000000000000000000000000000000000000002")
.unwrap();
let full_stamp_bytes = hex::decode(
"000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000003496cb9ac06221d39c3f6a7dd3b9c2301c1f923162b90d5443e42023f34ff908945b0da1c297190f111b7c6ebc828648ead8f7fce06c0364cb5a833410230c5c01c"
).unwrap();
let chunk_address = SwarmAddress::new(chunk_addr_bytes.try_into().unwrap());
let stamp = Stamp::try_from_slice(&full_stamp_bytes).unwrap();
let pubkey = stamp.recover_pubkey(&chunk_address).unwrap();
let result = stamp.verify_with_pubkey(&chunk_address, &pubkey);
assert!(result.is_ok());
}
#[test]
fn test_verify_with_wrong_pubkey() {
use alloy_signer::SignerSync;
use alloy_signer_local::PrivateKeySigner;
let signer = PrivateKeySigner::random();
let chunk_address = SwarmAddress::new([0xAB; 32]);
let batch_id = B256::ZERO;
let index = StampIndex::new(0, 0);
let timestamp = 12345u64;
let digest = StampDigest::new(chunk_address, batch_id, index, timestamp);
let prehash = digest.to_prehash();
let sig = signer.sign_message_sync(prehash.as_slice()).unwrap();
let stamp = Stamp::with_index(batch_id, index, timestamp, sig);
let correct_pubkey = stamp.recover_pubkey(&chunk_address).unwrap();
let wrong_signer = PrivateKeySigner::random();
let wrong_pubkey = wrong_signer.credential().verifying_key();
assert!(
stamp
.verify_with_pubkey(&chunk_address, &correct_pubkey)
.is_ok()
);
assert!(
stamp
.verify_with_pubkey(&chunk_address, wrong_pubkey)
.is_err()
);
}
}