unc_primitives/
transaction.rs

1use crate::errors::TxExecutionError;
2use crate::hash::{hash, CryptoHash};
3use crate::merkle::MerklePath;
4use crate::types::{AccountId, Balance, Gas, Nonce};
5use borsh::{BorshDeserialize, BorshSerialize};
6use serde::de::Error as DecodeError;
7use serde::ser::Error as EncodeError;
8use std::borrow::Borrow;
9use std::fmt;
10use std::hash::{Hash, Hasher};
11use unc_crypto::{PublicKey, Signature};
12use unc_fmt::{AbbrBytes, Slice};
13use unc_primitives_core::serialize::{from_base64, to_base64};
14use unc_primitives_core::types::Compute;
15use unc_vm_runner::{ProfileDataV2, ProfileDataV3};
16
17pub use crate::action::{
18    Action, AddKeyAction, CreateAccountAction, CreateRsa2048ChallengeAction, DeleteAccountAction,
19    DeleteKeyAction, DeployContractAction, FunctionCallAction, PledgeAction,
20    RegisterRsa2048KeysAction, TransferAction,
21};
22
23pub type LogEntry = String;
24
25#[derive(BorshSerialize, BorshDeserialize, serde::Serialize, PartialEq, Eq, Debug, Clone)]
26pub struct Transaction {
27    /// An account on which behalf transaction is signed
28    pub signer_id: AccountId,
29    /// A public key of the access key which was used to sign an account.
30    /// Access key holds permissions for calling certain kinds of actions.
31    pub public_key: PublicKey,
32    /// Nonce is used to determine order of transaction in the pool.
33    /// It increments for a combination of `signer_id` and `public_key`
34    pub nonce: Nonce,
35    /// Receiver account for this transaction
36    pub receiver_id: AccountId,
37    /// The hash of the block in the blockchain on top of which the given transaction is valid
38    pub block_hash: CryptoHash,
39    /// A list of actions to be applied
40    pub actions: Vec<Action>,
41}
42
43impl Transaction {
44    /// Computes a hash of the transaction for signing and size of serialized transaction
45    pub fn get_hash_and_size(&self) -> (CryptoHash, u64) {
46        let bytes = borsh::to_vec(&self).expect("Failed to deserialize");
47        (hash(&bytes), bytes.len() as u64)
48    }
49}
50
51#[derive(BorshSerialize, BorshDeserialize, Eq, Debug, Clone)]
52#[borsh(init=init)]
53pub struct SignedTransaction {
54    pub transaction: Transaction,
55    pub signature: Signature,
56    #[borsh(skip)]
57    hash: CryptoHash,
58    #[borsh(skip)]
59    size: u64,
60}
61
62impl SignedTransaction {
63    pub fn new(signature: Signature, transaction: Transaction) -> Self {
64        let mut signed_tx =
65            Self { signature, transaction, hash: CryptoHash::default(), size: u64::default() };
66        signed_tx.init();
67        signed_tx
68    }
69
70    pub fn init(&mut self) {
71        let (hash, size) = self.transaction.get_hash_and_size();
72        self.hash = hash;
73        self.size = size;
74    }
75
76    pub fn get_hash(&self) -> CryptoHash {
77        self.hash
78    }
79
80    pub fn get_size(&self) -> u64 {
81        self.size
82    }
83}
84
85impl Hash for SignedTransaction {
86    fn hash<H: Hasher>(&self, state: &mut H) {
87        self.hash.hash(state)
88    }
89}
90
91impl PartialEq for SignedTransaction {
92    fn eq(&self, other: &SignedTransaction) -> bool {
93        self.hash == other.hash && self.signature == other.signature
94    }
95}
96
97impl Borrow<CryptoHash> for SignedTransaction {
98    fn borrow(&self) -> &CryptoHash {
99        &self.hash
100    }
101}
102
103impl serde::Serialize for SignedTransaction {
104    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
105    where
106        S: serde::Serializer,
107    {
108        let signed_tx_borsh = borsh::to_vec(self).map_err(|err| {
109            S::Error::custom(&format!("the value could not be borsh encoded due to: {}", err))
110        })?;
111        let signed_tx_base64 = to_base64(&signed_tx_borsh);
112        serializer.serialize_str(&signed_tx_base64)
113    }
114}
115
116impl<'de> serde::Deserialize<'de> for SignedTransaction {
117    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
118    where
119        D: serde::Deserializer<'de>,
120    {
121        let signed_tx_base64 = <String as serde::Deserialize>::deserialize(deserializer)?;
122        let signed_tx_borsh = from_base64(&signed_tx_base64).map_err(|err| {
123            D::Error::custom(&format!("the value could not decoded from base64 due to: {}", err))
124        })?;
125        borsh::from_slice::<Self>(&signed_tx_borsh).map_err(|err| {
126            D::Error::custom(&format!("the value could not decoded from borsh due to: {}", err))
127        })
128    }
129}
130
131/// The status of execution for a transaction or a receipt.
132#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Clone, Default)]
133pub enum ExecutionStatus {
134    /// The execution is pending or unknown.
135    #[default]
136    Unknown,
137    /// The execution has failed with the given execution error.
138    Failure(TxExecutionError),
139    /// The final action succeeded and returned some value or an empty vec.
140    SuccessValue(Vec<u8>),
141    /// The final action of the receipt returned a promise or the signed transaction was converted
142    /// to a receipt. Contains the receipt_id of the generated receipt.
143    SuccessReceiptId(CryptoHash),
144}
145
146impl fmt::Debug for ExecutionStatus {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        match self {
149            ExecutionStatus::Unknown => f.write_str("Unknown"),
150            ExecutionStatus::Failure(e) => f.write_fmt(format_args!("Failure({})", e)),
151            ExecutionStatus::SuccessValue(v) => {
152                f.write_fmt(format_args!("SuccessValue({})", AbbrBytes(v)))
153            }
154            ExecutionStatus::SuccessReceiptId(receipt_id) => {
155                f.write_fmt(format_args!("SuccessReceiptId({})", receipt_id))
156            }
157        }
158    }
159}
160
161/// ExecutionOutcome for proof. Excludes logs and metadata
162#[derive(BorshSerialize, BorshDeserialize, PartialEq, Clone)]
163pub struct PartialExecutionOutcome {
164    pub receipt_ids: Vec<CryptoHash>,
165    pub gas_burnt: Gas,
166    pub tokens_burnt: Balance,
167    pub executor_id: AccountId,
168    pub status: PartialExecutionStatus,
169}
170
171impl From<&ExecutionOutcome> for PartialExecutionOutcome {
172    fn from(outcome: &ExecutionOutcome) -> Self {
173        Self {
174            receipt_ids: outcome.receipt_ids.clone(),
175            gas_burnt: outcome.gas_burnt,
176            tokens_burnt: outcome.tokens_burnt,
177            executor_id: outcome.executor_id.clone(),
178            status: outcome.status.clone().into(),
179        }
180    }
181}
182
183/// ExecutionStatus for proof. Excludes failure debug info.
184#[derive(BorshSerialize, BorshDeserialize, PartialEq, Clone)]
185pub enum PartialExecutionStatus {
186    Unknown,
187    Failure,
188    SuccessValue(Vec<u8>),
189    SuccessReceiptId(CryptoHash),
190}
191
192impl From<ExecutionStatus> for PartialExecutionStatus {
193    fn from(status: ExecutionStatus) -> PartialExecutionStatus {
194        match status {
195            ExecutionStatus::Unknown => PartialExecutionStatus::Unknown,
196            ExecutionStatus::Failure(_) => PartialExecutionStatus::Failure,
197            ExecutionStatus::SuccessValue(value) => PartialExecutionStatus::SuccessValue(value),
198            ExecutionStatus::SuccessReceiptId(id) => PartialExecutionStatus::SuccessReceiptId(id),
199        }
200    }
201}
202
203/// Execution outcome for one signed transaction or one receipt.
204#[derive(BorshSerialize, BorshDeserialize, PartialEq, Clone, smart_default::SmartDefault, Eq)]
205pub struct ExecutionOutcome {
206    /// Logs from this transaction or receipt.
207    pub logs: Vec<LogEntry>,
208    /// Receipt IDs generated by this transaction or receipt.
209    pub receipt_ids: Vec<CryptoHash>,
210    /// The amount of the gas burnt by the given transaction or receipt.
211    pub gas_burnt: Gas,
212    /// The amount of compute time spent by the given transaction or receipt.
213    // TODO(#8859): Treat this field in the same way as `gas_burnt`.
214    // At the moment this field is only set at runtime and is not persisted in the database.
215    // This means that when execution outcomes are read from the database, this value will not be
216    // set and any code that attempts to use it will crash.
217    #[borsh(skip)]
218    pub compute_usage: Option<Compute>,
219    /// The amount of tokens burnt corresponding to the burnt gas amount.
220    /// This value doesn't always equal to the `gas_burnt` multiplied by the gas price, because
221    /// the prepaid gas price might be lower than the actual gas price and it creates a deficit.
222    pub tokens_burnt: Balance,
223    /// The id of the account on which the execution happens. For transaction this is signer_id,
224    /// for receipt this is receiver_id.
225    #[default("test".parse().unwrap())]
226    pub executor_id: AccountId,
227    /// Execution status. Contains the result in case of successful execution.
228    /// NOTE: Should be the latest field since it contains unparsable by light client
229    /// ExecutionStatus::Failure
230    pub status: ExecutionStatus,
231    /// Execution metadata, versioned
232    pub metadata: ExecutionMetadata,
233}
234
235#[derive(BorshSerialize, BorshDeserialize, PartialEq, Clone, Eq, Debug, Default)]
236pub enum ExecutionMetadata {
237    /// V1: Empty Metadata
238    #[default]
239    V1,
240    /// V2: With ProfileData by legacy `Cost` enum
241    V2(ProfileDataV2),
242    /// V3: With ProfileData by gas parameters
243    V3(Box<ProfileDataV3>),
244}
245
246impl fmt::Debug for ExecutionOutcome {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        f.debug_struct("ExecutionOutcome")
249            .field("logs", &Slice(&self.logs))
250            .field("receipt_ids", &Slice(&self.receipt_ids))
251            .field("burnt_gas", &self.gas_burnt)
252            .field("compute_usage", &self.compute_usage.unwrap_or_default())
253            .field("tokens_burnt", &self.tokens_burnt)
254            .field("status", &self.status)
255            .field("metadata", &self.metadata)
256            .finish()
257    }
258}
259
260/// Execution outcome with the identifier.
261/// For a signed transaction, the ID is the hash of the transaction.
262/// For a receipt, the ID is the receipt ID.
263#[derive(PartialEq, Clone, Default, Debug, BorshSerialize, BorshDeserialize, Eq)]
264pub struct ExecutionOutcomeWithId {
265    /// The transaction hash or the receipt ID.
266    pub id: CryptoHash,
267    /// Should be the latest field since contains unparsable by light client ExecutionStatus::Failure
268    pub outcome: ExecutionOutcome,
269}
270
271impl ExecutionOutcomeWithId {
272    pub fn to_hashes(&self) -> Vec<CryptoHash> {
273        let mut result = Vec::with_capacity(2 + self.outcome.logs.len());
274        result.push(self.id);
275        result.push(CryptoHash::hash_borsh(PartialExecutionOutcome::from(&self.outcome)));
276        result.extend(self.outcome.logs.iter().map(|log| hash(log.as_bytes())));
277        result
278    }
279}
280
281/// Execution outcome with path from it to the outcome root and ID.
282#[derive(PartialEq, Clone, Default, Debug, BorshSerialize, BorshDeserialize, Eq)]
283pub struct ExecutionOutcomeWithIdAndProof {
284    pub proof: MerklePath,
285    pub block_hash: CryptoHash,
286    /// Should be the latest field since contains unparsable by light client ExecutionStatus::Failure
287    pub outcome_with_id: ExecutionOutcomeWithId,
288}
289
290impl ExecutionOutcomeWithIdAndProof {
291    pub fn id(&self) -> &CryptoHash {
292        &self.outcome_with_id.id
293    }
294}
295
296pub fn verify_transaction_signature(
297    transaction: &SignedTransaction,
298    public_keys: &[PublicKey],
299) -> bool {
300    let hash = transaction.get_hash();
301    let hash = hash.as_ref();
302    public_keys.iter().any(|key| transaction.signature.verify(hash, key))
303}
304
305/// A more compact struct, just for storage.
306#[derive(Clone, BorshSerialize, BorshDeserialize, Debug)]
307pub struct ExecutionOutcomeWithProof {
308    pub proof: MerklePath,
309    pub outcome: ExecutionOutcome,
310}
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use crate::account::{AccessKey, AccessKeyPermission, FunctionCallPermission};
315    use borsh::BorshDeserialize;
316    use unc_crypto::{InMemorySigner, KeyType, Signature, Signer};
317
318    #[test]
319    fn test_verify_transaction() {
320        let signer = InMemorySigner::from_random("test".parse().unwrap(), KeyType::ED25519);
321        let transaction = Transaction {
322            signer_id: "test".parse().unwrap(),
323            public_key: signer.public_key(),
324            nonce: 0,
325            receiver_id: "test".parse().unwrap(),
326            block_hash: Default::default(),
327            actions: vec![],
328        }
329        .sign(&signer);
330        let wrong_public_key = PublicKey::from_seed(KeyType::ED25519, "wrong");
331        let valid_keys = vec![signer.public_key(), wrong_public_key.clone()];
332        assert!(verify_transaction_signature(&transaction, &valid_keys));
333
334        let invalid_keys = vec![wrong_public_key];
335        assert!(!verify_transaction_signature(&transaction, &invalid_keys));
336
337        let bytes = borsh::to_vec(&transaction).unwrap();
338        let decoded_tx = SignedTransaction::try_from_slice(&bytes).unwrap();
339        assert!(verify_transaction_signature(&decoded_tx, &valid_keys));
340    }
341
342    /// This test is change checker for a reason - we don't expect transaction format to change.
343    /// If it does - you MUST update all of the dependencies: like unc-infra.and other clients.
344    #[test]
345    fn test_serialize_transaction() {
346        let public_key: PublicKey = "22skMptHjFWNyuEWY22ftn2AbLPSYpmYwGJRGwpNHbTV".parse().unwrap();
347        let transaction = Transaction {
348            signer_id: "test.unc".parse().unwrap(),
349            public_key: public_key.clone(),
350            nonce: 1,
351            receiver_id: "123".parse().unwrap(),
352            block_hash: Default::default(),
353            actions: vec![
354                Action::CreateAccount(CreateAccountAction {}),
355                Action::DeployContract(DeployContractAction { code: vec![1, 2, 3] }),
356                Action::FunctionCall(Box::new(FunctionCallAction {
357                    method_name: "qqq".to_string(),
358                    args: vec![1, 2, 3],
359                    gas: 1_000,
360                    deposit: 1_000_000,
361                })),
362                Action::Transfer(TransferAction { deposit: 123 }),
363                Action::Pledge(Box::new(PledgeAction {
364                    public_key: public_key.clone(),
365                    pledge: 1_000_000,
366                })),
367                Action::AddKey(Box::new(AddKeyAction {
368                    public_key: public_key.clone(),
369                    access_key: AccessKey {
370                        nonce: 0,
371                        permission: AccessKeyPermission::FunctionCall(FunctionCallPermission {
372                            allowance: None,
373                            receiver_id: "zzz".parse().unwrap(),
374                            method_names: vec!["www".to_string()],
375                        }),
376                    },
377                })),
378                Action::DeleteKey(Box::new(DeleteKeyAction { public_key })),
379                Action::DeleteAccount(DeleteAccountAction {
380                    beneficiary_id: "123".parse().unwrap(),
381                }),
382            ],
383        };
384        let signed_tx = SignedTransaction::new(Signature::empty(KeyType::ED25519), transaction);
385        let new_signed_tx =
386            SignedTransaction::try_from_slice(&borsh::to_vec(&signed_tx).unwrap()).unwrap();
387
388        assert_eq!(
389            new_signed_tx.get_hash().to_string(),
390            "4GXvjMFN6wSxnU9jEVT8HbXP5Yk6yELX9faRSKp6n9fX"
391        );
392    }
393
394    #[test]
395    fn test_outcome_to_hashes() {
396        let outcome = ExecutionOutcome {
397            status: ExecutionStatus::SuccessValue(vec![123]),
398            logs: vec!["123".to_string(), "321".to_string()],
399            receipt_ids: vec![],
400            gas_burnt: 123,
401            compute_usage: Some(456),
402            tokens_burnt: 1234000,
403            executor_id: "alice".parse().unwrap(),
404            metadata: ExecutionMetadata::V1,
405        };
406        let id = CryptoHash([42u8; 32]);
407        let outcome = ExecutionOutcomeWithId { id, outcome };
408        assert_eq!(
409            vec![
410                id,
411                "5JQs5ekQqKudMmYejuccbtEu1bzhQPXa92Zm4HdV64dQ".parse().unwrap(),
412                hash("123".as_bytes()),
413                hash("321".as_bytes()),
414            ],
415            outcome.to_hashes()
416        );
417    }
418}