use alloc::format;
use alloc::vec::Vec;
use crate::account::AccountDelta;
use crate::block::BlockNumber;
use crate::crypto::SequentialCommit;
use crate::errors::TransactionSummaryError;
use crate::transaction::{InputNote, InputNotes, RawOutputNotes};
use crate::utils::serde::{
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Serializable,
};
use crate::{Felt, WORD_SIZE, Word};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransactionSummary {
account_delta: AccountDelta,
input_notes: InputNotes<InputNote>,
output_notes: RawOutputNotes,
block_number: BlockNumber,
block_commitment: Word,
expiration_delta: u16,
user_params: TransactionSummaryUserParams,
}
impl TransactionSummary {
pub(crate) const VERSION: u8 = 1;
const VERSION_IDX: usize = 0;
const METADATA_IDX: usize = 1;
const USER_PARAMS_IDX: usize = 2;
pub const NUM_ELEMENTS: usize =
Self::USER_PARAMS_IDX + TransactionSummaryUserParams::NUM_ELEMENTS + 4 * WORD_SIZE;
pub fn new(
account_delta: AccountDelta,
input_notes: InputNotes<InputNote>,
output_notes: RawOutputNotes,
block_number: BlockNumber,
block_commitment: Word,
expiration_delta: u16,
user_params: TransactionSummaryUserParams,
) -> Self {
Self {
account_delta,
input_notes,
output_notes,
block_number,
block_commitment,
expiration_delta,
user_params,
}
}
pub fn account_delta(&self) -> &AccountDelta {
&self.account_delta
}
pub fn input_notes(&self) -> &InputNotes<InputNote> {
&self.input_notes
}
pub fn output_notes(&self) -> &RawOutputNotes {
&self.output_notes
}
pub fn block_number(&self) -> BlockNumber {
self.block_number
}
pub fn block_commitment(&self) -> Word {
self.block_commitment
}
pub fn expiration_delta(&self) -> u16 {
self.expiration_delta
}
pub fn metadata(&self) -> TransactionSummaryMetadata {
TransactionSummaryMetadata::new(self.block_number, self.expiration_delta)
}
pub fn user_params(&self) -> TransactionSummaryUserParams {
self.user_params
}
pub fn to_elements(&self) -> Vec<Felt> {
<Self as SequentialCommit>::to_elements(self)
}
pub fn to_commitment(&self) -> Word {
<Self as SequentialCommit>::to_commitment(self)
}
pub fn try_params_from_elements(
elements: &[Felt],
) -> Result<(TransactionSummaryMetadata, TransactionSummaryUserParams), TransactionSummaryError>
{
if elements.len() != Self::NUM_ELEMENTS {
return Err(TransactionSummaryError::InvalidPreimageLength {
actual: elements.len(),
expected: Self::NUM_ELEMENTS,
});
}
let version = elements[Self::VERSION_IDX];
if version != Felt::from(Self::VERSION) {
return Err(TransactionSummaryError::UnsupportedVersion {
actual: version,
expected: Self::VERSION,
});
}
let metadata = TransactionSummaryMetadata::try_from_element(elements[Self::METADATA_IDX])?;
let user_params_end = Self::USER_PARAMS_IDX + TransactionSummaryUserParams::NUM_ELEMENTS;
let user_params = elements[Self::USER_PARAMS_IDX..user_params_end]
.try_into()
.expect("preimage length was validated above");
Ok((metadata, TransactionSummaryUserParams::new(user_params)))
}
}
const _: () = assert!(TransactionSummary::NUM_ELEMENTS.is_multiple_of(WORD_SIZE));
impl SequentialCommit for TransactionSummary {
type Commitment = Word;
fn to_elements(&self) -> Vec<Felt> {
let mut elements = Vec::with_capacity(Self::NUM_ELEMENTS);
elements.push(Felt::from(Self::VERSION));
elements.push(self.metadata().to_element());
elements.extend_from_slice(self.user_params.as_elements());
elements.extend_from_slice(self.account_delta.to_commitment().as_elements());
elements.extend_from_slice(self.input_notes.commitment().as_elements());
elements.extend_from_slice(self.output_notes.commitment().as_elements());
elements.extend_from_slice(self.block_commitment.as_elements());
elements
}
}
impl Serializable for TransactionSummary {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
Self::VERSION.write_into(target);
self.account_delta.write_into(target);
self.input_notes.write_into(target);
self.output_notes.write_into(target);
self.block_number.write_into(target);
self.block_commitment.write_into(target);
self.expiration_delta.write_into(target);
self.user_params.write_into(target);
}
}
impl Deserializable for TransactionSummary {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let version: u8 = source.read()?;
if version != Self::VERSION {
return Err(DeserializationError::InvalidValue(format!(
"transaction summary version is {version} but only version {} is supported",
Self::VERSION
)));
}
let account_delta = source.read()?;
let input_notes = source.read()?;
let output_notes = source.read()?;
let block_number = source.read()?;
let block_commitment = source.read()?;
let expiration_delta = source.read()?;
let user_params = source.read()?;
Ok(Self::new(
account_delta,
input_notes,
output_notes,
block_number,
block_commitment,
expiration_delta,
user_params,
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TransactionSummaryMetadata {
block_number: BlockNumber,
expiration_delta: u16,
}
impl TransactionSummaryMetadata {
const EXPIRATION_DELTA_SHIFT: u32 = u32::BITS;
const NUM_BITS: u32 = Self::EXPIRATION_DELTA_SHIFT + u16::BITS;
pub fn new(block_number: BlockNumber, expiration_delta: u16) -> Self {
Self { block_number, expiration_delta }
}
pub fn try_from_element(metadata: Felt) -> Result<Self, TransactionSummaryError> {
let packed = metadata.as_canonical_u64();
if packed >> Self::NUM_BITS != 0 {
return Err(TransactionSummaryError::MetadataOutOfRange(metadata));
}
let block_number = BlockNumber::from(packed as u32);
let expiration_delta = (packed >> Self::EXPIRATION_DELTA_SHIFT) as u16;
Ok(Self::new(block_number, expiration_delta))
}
pub fn block_number(&self) -> BlockNumber {
self.block_number
}
pub fn expiration_delta(&self) -> u16 {
self.expiration_delta
}
pub fn to_element(&self) -> Felt {
let packed = (u64::from(self.expiration_delta) << Self::EXPIRATION_DELTA_SHIFT)
| u64::from(self.block_number.as_u32());
Felt::try_from(packed).expect("packed metadata should fit in felt")
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct TransactionSummaryUserParams {
elements: [Felt; Self::NUM_ELEMENTS],
}
impl TransactionSummaryUserParams {
pub const NUM_ELEMENTS: usize = 6;
pub fn new(elements: [Felt; Self::NUM_ELEMENTS]) -> Self {
Self { elements }
}
pub fn as_elements(&self) -> &[Felt; Self::NUM_ELEMENTS] {
&self.elements
}
}
impl Serializable for TransactionSummaryUserParams {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.elements.write_into(target);
}
}
impl Deserializable for TransactionSummaryUserParams {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
Ok(Self::new(source.read()?))
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use super::*;
use crate::ONE;
use crate::account::{AccountId, AccountStoragePatch, AccountVaultDelta};
use crate::testing::account_id::ACCOUNT_ID_PRIVATE_SENDER;
const BLOCK_NUMBER: u32 = 123;
const EXPIRATION_DELTA: u16 = 42;
const USER_PARAMS: [u32; TransactionSummaryUserParams::NUM_ELEMENTS] = [1, 2, 3, 4, 5, 6];
fn mock_summary() -> TransactionSummary {
let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
let account_delta = AccountDelta::new(
account_id,
AccountStoragePatch::new(),
AccountVaultDelta::default(),
None,
ONE,
)
.unwrap();
TransactionSummary::new(
account_delta,
InputNotes::new(Vec::new()).unwrap(),
RawOutputNotes::new(Vec::new()).unwrap(),
BlockNumber::from(BLOCK_NUMBER),
Word::from([9u32, 10, 11, 12].map(Felt::from)),
EXPIRATION_DELTA,
TransactionSummaryUserParams::new(USER_PARAMS.map(Felt::from)),
)
}
#[test]
fn tx_summary_params_element_roundtrip() -> anyhow::Result<()> {
let summary = mock_summary();
let elements = summary.to_elements();
assert_eq!(elements.len(), TransactionSummary::NUM_ELEMENTS);
assert_eq!(
&elements[..TransactionSummary::USER_PARAMS_IDX + USER_PARAMS.len()],
[
Felt::from(TransactionSummary::VERSION),
summary.metadata().to_element(),
Felt::from(USER_PARAMS[0]),
Felt::from(USER_PARAMS[1]),
Felt::from(USER_PARAMS[2]),
Felt::from(USER_PARAMS[3]),
Felt::from(USER_PARAMS[4]),
Felt::from(USER_PARAMS[5]),
]
);
let (metadata, user_params) = TransactionSummary::try_params_from_elements(&elements)?;
assert_eq!(metadata, summary.metadata());
assert_eq!(user_params, summary.user_params());
Ok(())
}
#[test]
fn tx_summary_serde_roundtrip() -> anyhow::Result<()> {
let summary = mock_summary();
let deserialized = TransactionSummary::read_from_bytes(&summary.to_bytes())?;
assert_eq!(deserialized, summary);
Ok(())
}
#[rstest::rstest]
#[case::genesis(0, 0)]
#[case::typical(BLOCK_NUMBER, EXPIRATION_DELTA)]
#[case::maximum(u32::MAX, u16::MAX)]
fn tx_summary_metadata_roundtrip(
#[case] block_number: u32,
#[case] expiration_delta: u16,
) -> anyhow::Result<()> {
let metadata =
TransactionSummaryMetadata::new(BlockNumber::from(block_number), expiration_delta);
let decoded = TransactionSummaryMetadata::try_from_element(metadata.to_element())?;
assert_eq!(decoded, metadata);
assert_eq!(decoded.block_number().as_u32(), block_number);
assert_eq!(decoded.expiration_delta(), expiration_delta);
Ok(())
}
#[test]
fn tx_summary_params_reject_unsupported_version() {
let unsupported_version = Felt::from(TransactionSummary::VERSION + 1);
let mut elements = mock_summary().to_elements();
elements[TransactionSummary::VERSION_IDX] = unsupported_version;
assert_matches!(
TransactionSummary::try_params_from_elements(&elements),
Err(TransactionSummaryError::UnsupportedVersion { actual, expected })
if actual == unsupported_version && expected == TransactionSummary::VERSION
);
}
#[test]
fn tx_summary_metadata_rejects_bits_above_packed_fields() {
let out_of_range = Felt::new_unchecked(1u64 << TransactionSummaryMetadata::NUM_BITS);
assert_matches!(
TransactionSummaryMetadata::try_from_element(out_of_range),
Err(TransactionSummaryError::MetadataOutOfRange(_))
);
}
#[test]
fn tx_summary_params_reject_preimage_of_wrong_length() {
let mut elements = mock_summary().to_elements();
elements.pop();
assert_matches!(
TransactionSummary::try_params_from_elements(&elements),
Err(TransactionSummaryError::InvalidPreimageLength { actual, expected })
if actual == TransactionSummary::NUM_ELEMENTS - 1
&& expected == TransactionSummary::NUM_ELEMENTS
);
}
}