use crate::errors::TxExecutionError;
use crate::hash::{hash, CryptoHash};
use crate::merkle::MerklePath;
use crate::types::{AccountId, Balance, Gas, Nonce};
use borsh::{BorshDeserialize, BorshSerialize};
use unc_crypto::{PublicKey, Signature};
use unc_fmt::{AbbrBytes, Slice};
use unc_primitives_core::serialize::{from_base64, to_base64};
use unc_primitives_core::types::Compute;
use unc_vm_runner::{ProfileDataV2, ProfileDataV3};
use serde::de::Error as DecodeError;
use serde::ser::Error as EncodeError;
use std::borrow::Borrow;
use std::fmt;
use std::hash::{Hash, Hasher};
pub use crate::action::{
Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeleteKeyAction,
DeployContractAction, FunctionCallAction, PledgeAction, TransferAction,
RegisterRsa2048KeysAction, CreateRsa2048ChallengeAction,
};
pub type LogEntry = String;
#[derive(BorshSerialize, BorshDeserialize, serde::Serialize, PartialEq, Eq, Debug, Clone)]
pub struct Transaction {
pub signer_id: AccountId,
pub public_key: PublicKey,
pub nonce: Nonce,
pub receiver_id: AccountId,
pub block_hash: CryptoHash,
pub actions: Vec<Action>,
}
impl Transaction {
pub fn get_hash_and_size(&self) -> (CryptoHash, u64) {
let bytes = borsh::to_vec(&self).expect("Failed to deserialize");
(hash(&bytes), bytes.len() as u64)
}
}
#[derive(BorshSerialize, BorshDeserialize, Eq, Debug, Clone)]
#[borsh(init=init)]
pub struct SignedTransaction {
pub transaction: Transaction,
pub signature: Signature,
#[borsh(skip)]
hash: CryptoHash,
#[borsh(skip)]
size: u64,
}
impl SignedTransaction {
pub fn new(signature: Signature, transaction: Transaction) -> Self {
let mut signed_tx =
Self { signature, transaction, hash: CryptoHash::default(), size: u64::default() };
signed_tx.init();
signed_tx
}
pub fn init(&mut self) {
let (hash, size) = self.transaction.get_hash_and_size();
self.hash = hash;
self.size = size;
}
pub fn get_hash(&self) -> CryptoHash {
self.hash
}
pub fn get_size(&self) -> u64 {
self.size
}
}
impl Hash for SignedTransaction {
fn hash<H: Hasher>(&self, state: &mut H) {
self.hash.hash(state)
}
}
impl PartialEq for SignedTransaction {
fn eq(&self, other: &SignedTransaction) -> bool {
self.hash == other.hash && self.signature == other.signature
}
}
impl Borrow<CryptoHash> for SignedTransaction {
fn borrow(&self) -> &CryptoHash {
&self.hash
}
}
impl serde::Serialize for SignedTransaction {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let signed_tx_borsh = borsh::to_vec(self).map_err(|err| {
S::Error::custom(&format!("the value could not be borsh encoded due to: {}", err))
})?;
let signed_tx_base64 = to_base64(&signed_tx_borsh);
serializer.serialize_str(&signed_tx_base64)
}
}
impl<'de> serde::Deserialize<'de> for SignedTransaction {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let signed_tx_base64 = <String as serde::Deserialize>::deserialize(deserializer)?;
let signed_tx_borsh = from_base64(&signed_tx_base64).map_err(|err| {
D::Error::custom(&format!("the value could not decoded from base64 due to: {}", err))
})?;
borsh::from_slice::<Self>(&signed_tx_borsh).map_err(|err| {
D::Error::custom(&format!("the value could not decoded from borsh due to: {}", err))
})
}
}
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Clone, Default)]
pub enum ExecutionStatus {
#[default]
Unknown,
Failure(TxExecutionError),
SuccessValue(Vec<u8>),
SuccessReceiptId(CryptoHash),
}
impl fmt::Debug for ExecutionStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ExecutionStatus::Unknown => f.write_str("Unknown"),
ExecutionStatus::Failure(e) => f.write_fmt(format_args!("Failure({})", e)),
ExecutionStatus::SuccessValue(v) => {
f.write_fmt(format_args!("SuccessValue({})", AbbrBytes(v)))
}
ExecutionStatus::SuccessReceiptId(receipt_id) => {
f.write_fmt(format_args!("SuccessReceiptId({})", receipt_id))
}
}
}
}
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Clone)]
pub struct PartialExecutionOutcome {
pub receipt_ids: Vec<CryptoHash>,
pub gas_burnt: Gas,
pub tokens_burnt: Balance,
pub executor_id: AccountId,
pub status: PartialExecutionStatus,
}
impl From<&ExecutionOutcome> for PartialExecutionOutcome {
fn from(outcome: &ExecutionOutcome) -> Self {
Self {
receipt_ids: outcome.receipt_ids.clone(),
gas_burnt: outcome.gas_burnt,
tokens_burnt: outcome.tokens_burnt,
executor_id: outcome.executor_id.clone(),
status: outcome.status.clone().into(),
}
}
}
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Clone)]
pub enum PartialExecutionStatus {
Unknown,
Failure,
SuccessValue(Vec<u8>),
SuccessReceiptId(CryptoHash),
}
impl From<ExecutionStatus> for PartialExecutionStatus {
fn from(status: ExecutionStatus) -> PartialExecutionStatus {
match status {
ExecutionStatus::Unknown => PartialExecutionStatus::Unknown,
ExecutionStatus::Failure(_) => PartialExecutionStatus::Failure,
ExecutionStatus::SuccessValue(value) => PartialExecutionStatus::SuccessValue(value),
ExecutionStatus::SuccessReceiptId(id) => PartialExecutionStatus::SuccessReceiptId(id),
}
}
}
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Clone, smart_default::SmartDefault, Eq)]
pub struct ExecutionOutcome {
pub logs: Vec<LogEntry>,
pub receipt_ids: Vec<CryptoHash>,
pub gas_burnt: Gas,
#[borsh(skip)]
pub compute_usage: Option<Compute>,
pub tokens_burnt: Balance,
#[default("test".parse().unwrap())]
pub executor_id: AccountId,
pub status: ExecutionStatus,
pub metadata: ExecutionMetadata,
}
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Clone, Eq, Debug, Default)]
pub enum ExecutionMetadata {
#[default]
V1,
V2(ProfileDataV2),
V3(Box<ProfileDataV3>),
}
impl fmt::Debug for ExecutionOutcome {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ExecutionOutcome")
.field("logs", &Slice(&self.logs))
.field("receipt_ids", &Slice(&self.receipt_ids))
.field("burnt_gas", &self.gas_burnt)
.field("compute_usage", &self.compute_usage.unwrap_or_default())
.field("tokens_burnt", &self.tokens_burnt)
.field("status", &self.status)
.field("metadata", &self.metadata)
.finish()
}
}
#[derive(PartialEq, Clone, Default, Debug, BorshSerialize, BorshDeserialize, Eq)]
pub struct ExecutionOutcomeWithId {
pub id: CryptoHash,
pub outcome: ExecutionOutcome,
}
impl ExecutionOutcomeWithId {
pub fn to_hashes(&self) -> Vec<CryptoHash> {
let mut result = Vec::with_capacity(2 + self.outcome.logs.len());
result.push(self.id);
result.push(CryptoHash::hash_borsh(PartialExecutionOutcome::from(&self.outcome)));
result.extend(self.outcome.logs.iter().map(|log| hash(log.as_bytes())));
result
}
}
#[derive(PartialEq, Clone, Default, Debug, BorshSerialize, BorshDeserialize, Eq)]
pub struct ExecutionOutcomeWithIdAndProof {
pub proof: MerklePath,
pub block_hash: CryptoHash,
pub outcome_with_id: ExecutionOutcomeWithId,
}
impl ExecutionOutcomeWithIdAndProof {
pub fn id(&self) -> &CryptoHash {
&self.outcome_with_id.id
}
}
pub fn verify_transaction_signature(
transaction: &SignedTransaction,
public_keys: &[PublicKey],
) -> bool {
let hash = transaction.get_hash();
let hash = hash.as_ref();
public_keys.iter().any(|key| transaction.signature.verify(hash, key))
}
#[derive(Clone, BorshSerialize, BorshDeserialize, Debug)]
pub struct ExecutionOutcomeWithProof {
pub proof: MerklePath,
pub outcome: ExecutionOutcome,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::account::{AccessKey, AccessKeyPermission, FunctionCallPermission};
use borsh::BorshDeserialize;
use unc_crypto::{InMemorySigner, KeyType, Signature, Signer};
#[test]
fn test_verify_transaction() {
let signer = InMemorySigner::from_random("test".parse().unwrap(), KeyType::ED25519);
let transaction = Transaction {
signer_id: "test".parse().unwrap(),
public_key: signer.public_key(),
nonce: 0,
receiver_id: "test".parse().unwrap(),
block_hash: Default::default(),
actions: vec![],
}
.sign(&signer);
let wrong_public_key = PublicKey::from_seed(KeyType::ED25519, "wrong");
let valid_keys = vec![signer.public_key(), wrong_public_key.clone()];
assert!(verify_transaction_signature(&transaction, &valid_keys));
let invalid_keys = vec![wrong_public_key];
assert!(!verify_transaction_signature(&transaction, &invalid_keys));
let bytes = borsh::to_vec(&transaction).unwrap();
let decoded_tx = SignedTransaction::try_from_slice(&bytes).unwrap();
assert!(verify_transaction_signature(&decoded_tx, &valid_keys));
}
#[test]
fn test_serialize_transaction() {
let public_key: PublicKey = "22skMptHjFWNyuEWY22ftn2AbLPSYpmYwGJRGwpNHbTV".parse().unwrap();
let transaction = Transaction {
signer_id: "test.unc".parse().unwrap(),
public_key: public_key.clone(),
nonce: 1,
receiver_id: "123".parse().unwrap(),
block_hash: Default::default(),
actions: vec![
Action::CreateAccount(CreateAccountAction {}),
Action::DeployContract(DeployContractAction { code: vec![1, 2, 3] }),
Action::FunctionCall(Box::new(FunctionCallAction {
method_name: "qqq".to_string(),
args: vec![1, 2, 3],
gas: 1_000,
deposit: 1_000_000,
})),
Action::Transfer(TransferAction { deposit: 123 }),
Action::Pledge(Box::new(PledgeAction {
public_key: public_key.clone(),
pledge: 1_000_000,
})),
Action::AddKey(Box::new(AddKeyAction {
public_key: public_key.clone(),
access_key: AccessKey {
nonce: 0,
permission: AccessKeyPermission::FunctionCall(FunctionCallPermission {
allowance: None,
receiver_id: "zzz".parse().unwrap(),
method_names: vec!["www".to_string()],
}),
},
})),
Action::DeleteKey(Box::new(DeleteKeyAction { public_key })),
Action::DeleteAccount(DeleteAccountAction {
beneficiary_id: "123".parse().unwrap(),
}),
],
};
let signed_tx = SignedTransaction::new(Signature::empty(KeyType::ED25519), transaction);
let new_signed_tx =
SignedTransaction::try_from_slice(&borsh::to_vec(&signed_tx).unwrap()).unwrap();
assert_eq!(
new_signed_tx.get_hash().to_string(),
"4GXvjMFN6wSxnU9jEVT8HbXP5Yk6yELX9faRSKp6n9fX"
);
}
#[test]
fn test_outcome_to_hashes() {
let outcome = ExecutionOutcome {
status: ExecutionStatus::SuccessValue(vec![123]),
logs: vec!["123".to_string(), "321".to_string()],
receipt_ids: vec![],
gas_burnt: 123,
compute_usage: Some(456),
tokens_burnt: 1234000,
executor_id: "alice".parse().unwrap(),
metadata: ExecutionMetadata::V1,
};
let id = CryptoHash([42u8; 32]);
let outcome = ExecutionOutcomeWithId { id, outcome };
assert_eq!(
vec![
id,
"5JQs5ekQqKudMmYejuccbtEu1bzhQPXa92Zm4HdV64dQ".parse().unwrap(),
hash("123".as_bytes()),
hash("321".as_bytes()),
],
outcome.to_hashes()
);
}
}