pub mod arg_handling;
mod errors_v1;
pub mod fields_container;
mod transaction_args;
#[cfg(any(feature = "std", test))]
mod transaction_v1_builder;
mod transaction_v1_hash;
pub mod transaction_v1_payload;
#[cfg(any(feature = "std", feature = "testing", test))]
use super::InitiatorAddrAndSecretKey;
#[cfg(any(all(feature = "std", feature = "testing"), test))]
use super::{TransactionEntryPoint, TransactionTarget};
use crate::{
bytesrepr::{self, Error, FromBytes, ToBytes},
crypto,
};
#[cfg(any(all(feature = "std", feature = "testing"), test))]
use crate::{
testing::TestRng, AUCTION_LANE_ID, INSTALL_UPGRADE_LANE_ID, LARGE_WASM_LANE_ID, MINT_LANE_ID,
};
#[cfg(any(feature = "std", test, feature = "testing"))]
use alloc::collections::BTreeMap;
use alloc::{collections::BTreeSet, vec::Vec};
#[cfg(feature = "datasize")]
use datasize::DataSize;
use errors_v1::FieldDeserializationError;
#[cfg(any(all(feature = "std", feature = "testing"), test))]
use fields_container::{ENTRY_POINT_MAP_KEY, TARGET_MAP_KEY};
#[cfg(any(feature = "once_cell", test))]
use once_cell::sync::OnceCell;
#[cfg(feature = "json-schema")]
use schemars::JsonSchema;
#[cfg(any(feature = "std", test))]
use serde::{Deserialize, Serialize};
#[cfg(any(feature = "std", test))]
use thiserror::Error;
use tracing::debug;
pub use transaction_v1_payload::TransactionV1Payload;
#[cfg(any(feature = "std", test))]
use transaction_v1_payload::TransactionV1PayloadJson;
use super::{
serialization::{CalltableSerializationEnvelope, CalltableSerializationEnvelopeBuilder},
Approval, ApprovalsHash, InitiatorAddr, PricingMode,
};
#[cfg(any(feature = "std", feature = "testing", test))]
use crate::bytesrepr::Bytes;
use crate::{Digest, DisplayIter, SecretKey, TimeDiff, Timestamp};
pub use errors_v1::{
DecodeFromJsonErrorV1 as TransactionV1DecodeFromJsonError, ErrorV1 as TransactionV1Error,
ExcessiveSizeErrorV1 as TransactionV1ExcessiveSizeError,
InvalidTransaction as InvalidTransactionV1,
};
pub use transaction_args::TransactionArgs;
#[cfg(any(feature = "std", test))]
pub use transaction_v1_builder::{TransactionV1Builder, TransactionV1BuilderError};
pub use transaction_v1_hash::TransactionV1Hash;
use core::{
cmp,
fmt::{self, Debug, Display, Formatter},
hash,
};
const HASH_FIELD_INDEX: u16 = 0;
const PAYLOAD_FIELD_INDEX: u16 = 1;
const APPROVALS_FIELD_INDEX: u16 = 2;
#[derive(Clone, Eq, Debug)]
#[cfg_attr(any(feature = "std", test), derive(Serialize, Deserialize))]
#[cfg_attr(feature = "datasize", derive(DataSize))]
#[cfg_attr(
feature = "json-schema",
derive(JsonSchema),
schemars(with = "TransactionV1Json")
)]
pub struct TransactionV1 {
hash: TransactionV1Hash,
payload: TransactionV1Payload,
approvals: BTreeSet<Approval>,
#[cfg_attr(any(all(feature = "std", feature = "once_cell"), test), serde(skip))]
#[cfg_attr(
all(any(feature = "once_cell", test), feature = "datasize"),
data_size(skip)
)]
#[cfg(any(feature = "once_cell", test))]
is_verified: OnceCell<Result<(), InvalidTransactionV1>>,
}
#[cfg(any(feature = "std", test))]
impl TryFrom<TransactionV1Json> for TransactionV1 {
type Error = TransactionV1JsonError;
fn try_from(transaction_v1_json: TransactionV1Json) -> Result<Self, Self::Error> {
Ok(TransactionV1 {
hash: transaction_v1_json.hash,
payload: transaction_v1_json.payload.try_into().map_err(|error| {
TransactionV1JsonError::FailedToMap(format!(
"Failed to map TransactionJson::V1 to Transaction::V1, err: {}",
error
))
})?,
approvals: transaction_v1_json.approvals,
#[cfg(any(feature = "once_cell", test))]
is_verified: OnceCell::new(),
})
}
}
#[cfg(any(feature = "std", test))]
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(
feature = "json-schema",
derive(JsonSchema),
schemars(
description = "A unit of work sent by a client to the network, which when executed can \
cause global state to be altered.",
rename = "TransactionV1",
)
)]
pub(super) struct TransactionV1Json {
hash: TransactionV1Hash,
payload: TransactionV1PayloadJson,
approvals: BTreeSet<Approval>,
}
#[cfg(any(feature = "std", test))]
#[derive(Error, Debug)]
pub(super) enum TransactionV1JsonError {
#[error("{0}")]
FailedToMap(String),
}
#[cfg(any(feature = "std", test))]
impl TryFrom<TransactionV1> for TransactionV1Json {
type Error = TransactionV1JsonError;
fn try_from(transaction: TransactionV1) -> Result<Self, Self::Error> {
Ok(TransactionV1Json {
hash: transaction.hash,
payload: transaction.payload.try_into().map_err(|error| {
TransactionV1JsonError::FailedToMap(format!(
"Failed to map Transaction::V1 to TransactionJson::V1, err: {}",
error
))
})?,
approvals: transaction.approvals,
})
}
}
impl TransactionV1 {
#[cfg(any(feature = "std", test, feature = "testing"))]
pub(crate) fn build(
chain_name: String,
timestamp: Timestamp,
ttl: TimeDiff,
pricing_mode: PricingMode,
fields: BTreeMap<u16, Bytes>,
initiator_addr_and_secret_key: InitiatorAddrAndSecretKey,
) -> TransactionV1 {
let initiator_addr = initiator_addr_and_secret_key.initiator_addr();
let transaction_v1_payload = TransactionV1Payload::new(
chain_name,
timestamp,
ttl,
pricing_mode,
initiator_addr,
fields,
);
let hash = Digest::hash(
transaction_v1_payload
.to_bytes()
.unwrap_or_else(|error| panic!("should serialize body: {}", error)),
);
let mut transaction = TransactionV1 {
hash: hash.into(),
payload: transaction_v1_payload,
approvals: BTreeSet::new(),
#[cfg(any(feature = "once_cell", test))]
is_verified: OnceCell::new(),
};
if let Some(secret_key) = initiator_addr_and_secret_key.secret_key() {
transaction.sign(secret_key);
}
transaction
}
pub fn sign(&mut self, secret_key: &SecretKey) {
let approval = Approval::create(&self.hash.into(), secret_key);
self.approvals.insert(approval);
}
pub fn hash(&self) -> &TransactionV1Hash {
&self.hash
}
pub fn payload(&self) -> &TransactionV1Payload {
&self.payload
}
pub fn approvals(&self) -> &BTreeSet<Approval> {
&self.approvals
}
pub fn initiator_addr(&self) -> &InitiatorAddr {
self.payload.initiator_addr()
}
pub fn chain_name(&self) -> &str {
self.payload.chain_name()
}
pub fn timestamp(&self) -> Timestamp {
self.payload.timestamp()
}
pub fn ttl(&self) -> TimeDiff {
self.payload.ttl()
}
pub fn expired(&self, current_instant: Timestamp) -> bool {
self.payload.expired(current_instant)
}
pub fn pricing_mode(&self) -> &PricingMode {
self.payload.pricing_mode()
}
pub fn compute_approvals_hash(&self) -> Result<ApprovalsHash, bytesrepr::Error> {
ApprovalsHash::compute(&self.approvals)
}
#[doc(hidden)]
pub fn with_approvals(mut self, approvals: BTreeSet<Approval>) -> Self {
self.approvals = approvals;
self
}
#[cfg(any(all(feature = "std", feature = "testing"), test))]
pub(super) fn apply_approvals(&mut self, approvals: Vec<Approval>) {
self.approvals.extend(approvals);
}
#[cfg(any(all(feature = "std", feature = "testing"), test))]
pub fn random(rng: &mut TestRng) -> Self {
TransactionV1Builder::new_random(rng).build().unwrap()
}
#[cfg(any(all(feature = "std", feature = "testing"), test))]
pub fn random_transfer(
rng: &mut TestRng,
timestamp: Option<Timestamp>,
ttl: Option<TimeDiff>,
) -> Self {
let transaction_v1 = TransactionV1Builder::new_random_with_category_and_timestamp_and_ttl(
rng,
MINT_LANE_ID,
timestamp,
ttl,
)
.build()
.unwrap();
transaction_v1
}
#[cfg(any(all(feature = "std", feature = "testing"), test))]
pub fn random_wasm(
rng: &mut TestRng,
timestamp: Option<Timestamp>,
ttl: Option<TimeDiff>,
) -> Self {
let transaction = TransactionV1Builder::new_random_with_category_and_timestamp_and_ttl(
rng,
LARGE_WASM_LANE_ID,
timestamp,
ttl,
)
.build()
.unwrap();
transaction
}
#[cfg(any(all(feature = "std", feature = "testing"), test))]
pub fn random_auction(
rng: &mut TestRng,
timestamp: Option<Timestamp>,
ttl: Option<TimeDiff>,
) -> Self {
TransactionV1Builder::new_random_with_category_and_timestamp_and_ttl(
rng,
AUCTION_LANE_ID,
timestamp,
ttl,
)
.build()
.unwrap()
}
#[cfg(any(all(feature = "std", feature = "testing"), test))]
pub fn random_install_upgrade(
rng: &mut TestRng,
timestamp: Option<Timestamp>,
ttl: Option<TimeDiff>,
) -> Self {
let transaction = TransactionV1Builder::new_random_with_category_and_timestamp_and_ttl(
rng,
INSTALL_UPGRADE_LANE_ID,
timestamp,
ttl,
)
.build()
.unwrap();
transaction
}
pub fn deserialize_field<T: FromBytes>(
&self,
index: u16,
) -> Result<T, FieldDeserializationError> {
self.payload.deserialize_field(index)
}
pub fn number_of_fields(&self) -> usize {
self.payload.number_of_fields()
}
pub fn has_valid_hash(&self) -> Result<(), InvalidTransactionV1> {
let computed_hash = Digest::hash(
self.payload
.to_bytes()
.unwrap_or_else(|error| panic!("should serialize body: {}", error)),
);
if TransactionV1Hash::new(computed_hash) != self.hash {
debug!(?self, ?computed_hash, "invalid transaction hash");
return Err(InvalidTransactionV1::InvalidTransactionHash);
}
Ok(())
}
pub fn verify(&self) -> Result<(), InvalidTransactionV1> {
#[cfg(any(feature = "once_cell", test))]
return self.is_verified.get_or_init(|| self.do_verify()).clone();
#[cfg(not(any(feature = "once_cell", test)))]
self.do_verify()
}
fn do_verify(&self) -> Result<(), InvalidTransactionV1> {
if self.approvals.is_empty() {
debug!(?self, "transaction has no approvals");
return Err(InvalidTransactionV1::EmptyApprovals);
}
self.has_valid_hash()?;
for (index, approval) in self.approvals.iter().enumerate() {
if let Err(error) = crypto::verify(self.hash, approval.signature(), approval.signer()) {
debug!(
?self,
"failed to verify transaction approval {}: {}", index, error
);
return Err(InvalidTransactionV1::InvalidApproval { index, error });
}
}
Ok(())
}
pub fn payload_hash(&self) -> Result<Digest, InvalidTransactionV1> {
let bytes = self
.payload
.fields()
.to_bytes()
.map_err(|_| InvalidTransactionV1::CannotCalculateFieldsHash)?;
Ok(Digest::hash(bytes))
}
fn serialized_field_lengths(&self) -> Vec<usize> {
vec![
self.hash.serialized_length(),
self.payload.serialized_length(),
self.approvals.serialized_length(),
]
}
#[cfg(any(all(feature = "std", feature = "testing"), test))]
pub fn invalidate(&mut self) {
self.payload.invalidate();
}
#[cfg(any(all(feature = "std", feature = "testing"), test))]
pub(crate) fn get_transaction_target(&self) -> Result<TransactionTarget, InvalidTransactionV1> {
self.deserialize_field::<TransactionTarget>(TARGET_MAP_KEY)
.map_err(|error| InvalidTransactionV1::CouldNotDeserializeField { error })
}
#[cfg(any(all(feature = "std", feature = "testing"), test))]
pub(crate) fn get_transaction_entry_point(
&self,
) -> Result<TransactionEntryPoint, InvalidTransactionV1> {
self.deserialize_field::<TransactionEntryPoint>(ENTRY_POINT_MAP_KEY)
.map_err(|error| InvalidTransactionV1::CouldNotDeserializeField { error })
}
pub fn gas_price_tolerance(&self) -> u8 {
match self.pricing_mode() {
PricingMode::PaymentLimited {
gas_price_tolerance,
..
} => *gas_price_tolerance,
PricingMode::Fixed {
gas_price_tolerance,
..
} => *gas_price_tolerance,
PricingMode::Prepaid { .. } => {
0u8
}
}
}
}
impl ToBytes for TransactionV1 {
fn to_bytes(&self) -> Result<Vec<u8>, crate::bytesrepr::Error> {
let expected_payload_sizes = self.serialized_field_lengths();
CalltableSerializationEnvelopeBuilder::new(expected_payload_sizes)?
.add_field(HASH_FIELD_INDEX, &self.hash)?
.add_field(PAYLOAD_FIELD_INDEX, &self.payload)?
.add_field(APPROVALS_FIELD_INDEX, &self.approvals)?
.binary_payload_bytes()
}
fn serialized_length(&self) -> usize {
CalltableSerializationEnvelope::estimate_size(self.serialized_field_lengths())
}
}
impl FromBytes for TransactionV1 {
fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), Error> {
let (binary_payload, remainder) = CalltableSerializationEnvelope::from_bytes(3, bytes)?;
let window = binary_payload.start_consuming()?.ok_or(Error::Formatting)?;
window.verify_index(HASH_FIELD_INDEX)?;
let (hash, window) = window.deserialize_and_maybe_next::<TransactionV1Hash>()?;
let window = window.ok_or(Error::Formatting)?;
window.verify_index(PAYLOAD_FIELD_INDEX)?;
let (payload, window) = window.deserialize_and_maybe_next::<TransactionV1Payload>()?;
let window = window.ok_or(Error::Formatting)?;
window.verify_index(APPROVALS_FIELD_INDEX)?;
let (approvals, window) = window.deserialize_and_maybe_next::<BTreeSet<Approval>>()?;
if window.is_some() {
return Err(Error::Formatting);
}
let from_bytes = TransactionV1 {
hash,
payload,
approvals,
#[cfg(any(feature = "once_cell", test))]
is_verified: OnceCell::new(),
};
Ok((from_bytes, remainder))
}
}
impl Display for TransactionV1 {
fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
write!(
formatter,
"transaction-v1[{}, {}, approvals: {}]",
self.hash,
self.payload,
DisplayIter::new(self.approvals.iter())
)
}
}
impl hash::Hash for TransactionV1 {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
let TransactionV1 {
hash,
payload,
approvals,
#[cfg(any(feature = "once_cell", test))]
is_verified: _,
} = self;
hash.hash(state);
payload.hash(state);
approvals.hash(state);
}
}
impl PartialEq for TransactionV1 {
fn eq(&self, other: &TransactionV1) -> bool {
let TransactionV1 {
hash,
payload,
approvals,
#[cfg(any(feature = "once_cell", test))]
is_verified: _,
} = self;
*hash == other.hash && *payload == other.payload && *approvals == other.approvals
}
}
impl Ord for TransactionV1 {
fn cmp(&self, other: &TransactionV1) -> cmp::Ordering {
let TransactionV1 {
hash,
payload,
approvals,
#[cfg(any(feature = "once_cell", test))]
is_verified: _,
} = self;
hash.cmp(&other.hash)
.then_with(|| payload.cmp(&other.payload))
.then_with(|| approvals.cmp(&other.approvals))
}
}
impl PartialOrd for TransactionV1 {
fn partial_cmp(&self, other: &TransactionV1) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}