pub use crate::action::{
Action, AddKeyAction, CreateAccountAction, DeleteAccountAction, DeleteKeyAction,
DeployContractAction, FunctionCallAction, StakeAction, TransferAction,
};
use crate::errors::{InvalidTxError, TxExecutionError};
use crate::hash::{CryptoHash, hash};
use crate::merkle::MerklePath;
use crate::profile_data_v3::ProfileDataV3;
use crate::types::{AccountId, Balance, Gas, Nonce};
use borsh::{BorshDeserialize, BorshSerialize};
use near_crypto::{PublicKey, Signature};
use near_fmt::{AbbrBytes, Slice};
use near_parameters::RuntimeConfig;
use near_primitives_core::serialize::{from_base64, to_base64};
use near_primitives_core::types::{Compute, NonceIndex, ProtocolVersion};
use near_primitives_core::version::ProtocolFeature;
use near_schema_checker_lib::ProtocolSchema;
#[cfg(feature = "schemars")]
use schemars::json_schema;
use serde::de::Error as DecodeError;
use serde::ser::Error as EncodeError;
use std::borrow::Borrow;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::io::{Error, ErrorKind, Read, Write};
pub type LogEntry = String;
#[derive(
BorshSerialize, BorshDeserialize, serde::Serialize, PartialEq, Eq, Debug, Clone, ProtocolSchema,
)]
pub struct TransactionV0 {
pub signer_id: AccountId,
pub public_key: PublicKey,
pub nonce: Nonce,
pub receiver_id: AccountId,
pub block_hash: CryptoHash,
pub actions: Vec<Action>,
}
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Copy, ProtocolSchema)]
pub enum TransactionNonce {
Nonce { nonce: Nonce },
GasKeyNonce { nonce: Nonce, nonce_index: NonceIndex },
}
impl TransactionNonce {
pub fn from_nonce(nonce: Nonce) -> Self {
TransactionNonce::Nonce { nonce }
}
pub fn from_nonce_and_index(nonce: Nonce, nonce_index: NonceIndex) -> Self {
TransactionNonce::GasKeyNonce { nonce, nonce_index }
}
pub fn nonce(&self) -> Nonce {
match self {
TransactionNonce::Nonce { nonce } => *nonce,
TransactionNonce::GasKeyNonce { nonce, .. } => *nonce,
}
}
pub fn nonce_index(&self) -> Option<NonceIndex> {
match self {
TransactionNonce::Nonce { .. } => None,
TransactionNonce::GasKeyNonce { nonce_index, .. } => Some(*nonce_index),
}
}
}
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, ProtocolSchema)]
pub struct TransactionV1 {
pub signer_id: AccountId,
pub public_key: PublicKey,
pub nonce: TransactionNonce,
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(Eq, PartialEq, Debug, Clone)]
pub enum Transaction {
V0(TransactionV0),
V1(TransactionV1),
}
impl Transaction {
pub fn signer_id(&self) -> &AccountId {
match self {
Transaction::V0(tx) => &tx.signer_id,
Transaction::V1(tx) => &tx.signer_id,
}
}
pub fn receiver_id(&self) -> &AccountId {
match self {
Transaction::V0(tx) => &tx.receiver_id,
Transaction::V1(tx) => &tx.receiver_id,
}
}
pub fn public_key(&self) -> &PublicKey {
match self {
Transaction::V0(tx) => &tx.public_key,
Transaction::V1(tx) => &tx.public_key,
}
}
pub fn nonce(&self) -> TransactionNonce {
match self {
Transaction::V0(tx) => TransactionNonce::from_nonce(tx.nonce),
Transaction::V1(tx) => tx.nonce,
}
}
pub fn actions(&self) -> &[Action] {
match self {
Transaction::V0(tx) => &tx.actions,
Transaction::V1(tx) => &tx.actions,
}
}
pub fn take_actions(self) -> Vec<Action> {
match self {
Transaction::V0(tx) => tx.actions,
Transaction::V1(tx) => tx.actions,
}
}
pub fn block_hash(&self) -> &CryptoHash {
match self {
Transaction::V0(tx) => &tx.block_hash,
Transaction::V1(tx) => &tx.block_hash,
}
}
pub fn gas_keys_required(&self) -> bool {
match self {
Transaction::V0(_) => false,
Transaction::V1(_) => true,
}
}
}
impl BorshSerialize for Transaction {
fn serialize<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
match self {
Transaction::V0(tx) => tx.serialize(writer)?,
Transaction::V1(tx) => {
BorshSerialize::serialize(&1_u8, writer)?;
tx.serialize(writer)?;
}
}
Ok(())
}
}
impl BorshDeserialize for Transaction {
fn deserialize_reader<R: Read>(reader: &mut R) -> std::io::Result<Self> {
let u1 = u8::deserialize_reader(reader)?;
let u2 = u8::deserialize_reader(reader)?;
if u2 == 0 {
let prefix = [u1, u2];
let mut reader = prefix.chain(reader);
let tx = TransactionV0::deserialize_reader(&mut reader)?;
return Ok(Transaction::V0(tx));
}
if u1 == 1 {
let prefix = [u2];
let mut reader = prefix.chain(reader);
let tx = TransactionV1::deserialize_reader(&mut reader)?;
return Ok(Transaction::V1(tx));
}
Err(Error::new(ErrorKind::InvalidData, format!("invalid transaction version tag: {}", u1)))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ValidatedTransaction(SignedTransaction);
impl ValidatedTransaction {
#[allow(clippy::result_large_err)]
pub fn new(
config: &RuntimeConfig,
signed_tx: SignedTransaction,
protocol_version: ProtocolVersion,
) -> Result<Self, (InvalidTxError, SignedTransaction)> {
match Self::check_valid_for_config(config, &signed_tx, protocol_version) {
Ok(()) => {}
Err(err) => return Err((err, signed_tx)),
}
if !signed_tx
.signature
.verify(signed_tx.get_hash().as_ref(), signed_tx.transaction.public_key())
{
return Err((InvalidTxError::InvalidSignature, signed_tx));
}
Ok(Self(signed_tx))
}
pub fn check_valid_for_config(
config: &RuntimeConfig,
signed_tx: &SignedTransaction,
protocol_version: ProtocolVersion,
) -> Result<(), InvalidTxError> {
if !ProtocolFeature::GasKeys.enabled(protocol_version)
&& signed_tx.transaction.gas_keys_required()
{
return Err(InvalidTxError::InvalidTransactionVersion);
}
let tx_size = signed_tx.get_size();
let max_tx_size = config.wasm_config.limit_config.max_transaction_size;
if tx_size > max_tx_size {
return Err(InvalidTxError::TransactionSizeExceeded {
size: tx_size,
limit: max_tx_size,
});
}
Ok(())
}
pub fn new_for_test(signed_tx: SignedTransaction) -> Self {
Self(signed_tx)
}
pub fn to_signed_tx(&self) -> &SignedTransaction {
&self.0
}
pub fn into_signed_tx(self) -> SignedTransaction {
self.0
}
pub fn to_tx(&self) -> &Transaction {
&self.0.transaction
}
pub fn get_hash(&self) -> CryptoHash {
self.0.get_hash()
}
pub fn get_size(&self) -> u64 {
self.0.get_size()
}
pub fn signer_id(&self) -> &AccountId {
self.to_tx().signer_id()
}
pub fn receiver_id(&self) -> &AccountId {
self.to_tx().receiver_id()
}
pub fn nonce(&self) -> TransactionNonce {
self.to_tx().nonce()
}
pub fn public_key(&self) -> &PublicKey {
self.to_tx().public_key()
}
pub fn actions(&self) -> &[Action] {
self.to_tx().actions()
}
}
#[derive(BorshSerialize, BorshDeserialize, Eq, Debug, Clone, ProtocolSchema)]
#[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 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))
})
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for SignedTransaction {
fn schema_name() -> std::borrow::Cow<'static, str> {
"SignedTransaction".to_string().into()
}
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
json_schema!({
"type": "string",
"format": "byte"
})
}
}
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Clone, Default, ProtocolSchema)]
#[borsh(use_discriminant = true)]
#[repr(u8)]
pub enum ExecutionStatus {
#[default]
Unknown = 0,
Failure(TxExecutionError) = 1,
SuccessValue(Vec<u8>) = 2,
SuccessReceiptId(CryptoHash) = 3,
}
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)]
#[borsh(use_discriminant = true)]
#[repr(u8)]
pub enum PartialExecutionStatus {
Unknown = 0,
Failure = 1,
SuccessValue(Vec<u8>) = 2,
SuccessReceiptId(CryptoHash) = 3,
}
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,
ProtocolSchema,
)]
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, ProtocolSchema,
)]
#[borsh(use_discriminant = true)]
#[repr(u8)]
pub enum ExecutionMetadata {
#[default]
V1 = 0,
V2(crate::profile_data_v2::ProfileDataV2) = 1,
V3(Box<ProfileDataV3>) = 2,
}
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, ProtocolSchema,
)]
pub struct ExecutionOutcomeWithId {
pub id: CryptoHash,
pub outcome: ExecutionOutcome,
}
impl ExecutionOutcomeWithId {
pub fn failed(transaction: &SignedTransaction, error: InvalidTxError) -> Self {
Self::failed_with_gas_burnt(transaction, error, Gas::ZERO, Balance::ZERO)
}
pub fn failed_with_gas_burnt(
transaction: &SignedTransaction,
error: InvalidTxError,
gas_burnt: Gas,
tokens_burnt: Balance,
) -> Self {
Self {
id: transaction.get_hash(),
outcome: ExecutionOutcome {
executor_id: transaction.transaction.signer_id().clone(),
status: ExecutionStatus::Failure(TxExecutionError::InvalidTxError(error)),
gas_burnt,
compute_usage: Some(gas_burnt.as_gas()),
tokens_burnt,
..Default::default()
},
}
}
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, ProtocolSchema,
)]
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, ProtocolSchema)]
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 near_crypto::{InMemorySigner, KeyType, Signature, Signer};
use near_primitives::types::Gas;
#[test]
fn test_verify_transaction() {
let signer: Signer =
InMemorySigner::from_random("test".parse().unwrap(), KeyType::ED25519).into();
let transaction = Transaction::V0(TransactionV0 {
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));
}
fn create_transaction_v0() -> TransactionV0 {
let public_key: PublicKey = "22skMptHjFWNyuEWY22ftn2AbLPSYpmYwGJRGwpNHbTV".parse().unwrap();
TransactionV0 {
signer_id: "test.near".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: Gas::from_gas(1_000),
deposit: Balance::from_yoctonear(1_000_000),
})),
Action::Transfer(TransferAction { deposit: Balance::from_yoctonear(123) }),
Action::Stake(Box::new(StakeAction {
public_key: public_key.clone(),
stake: Balance::from_yoctonear(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(),
}),
],
}
}
fn create_transaction_v1() -> TransactionV1 {
let public_key: PublicKey = "22skMptHjFWNyuEWY22ftn2AbLPSYpmYwGJRGwpNHbTV".parse().unwrap();
TransactionV1 {
signer_id: "test.near".parse().unwrap(),
public_key: public_key.clone(),
nonce: TransactionNonce::from_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: Gas::from_gas(1_000),
deposit: Balance::from_yoctonear(1_000_000),
})),
Action::Transfer(TransferAction { deposit: Balance::from_yoctonear(123) }),
Action::Stake(Box::new(StakeAction {
public_key: public_key.clone(),
stake: Balance::from_yoctonear(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(),
}),
],
}
}
#[test]
fn test_serialize_transaction() {
let transaction = Transaction::V0(create_transaction_v0());
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_serialize_transaction_versions() {
let transaction_v0 = Transaction::V0(create_transaction_v0());
let serialized_tx_v0 = borsh::to_vec(&transaction_v0).unwrap();
let deserialized_tx_v0 = Transaction::try_from_slice(&serialized_tx_v0).unwrap();
assert_eq!(transaction_v0, deserialized_tx_v0);
let transaction_v1 = Transaction::V1(create_transaction_v1());
let serialized_tx_v1 = borsh::to_vec(&transaction_v1).unwrap();
let deserialized_tx_v1 = Transaction::try_from_slice(&serialized_tx_v1).unwrap();
assert_eq!(transaction_v1, deserialized_tx_v1);
}
#[test]
fn test_deserialize_invalid_account_id() {
let mut serialized_tx = vec![];
serialized_tx.extend_from_slice(&10u32.to_le_bytes());
serialized_tx.extend_from_slice(&[0u8; 10]);
let result = Transaction::try_from_slice(&serialized_tx);
let err = result.unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
assert!(err.to_string().contains("the Account ID contains an invalid character"));
}
#[test]
fn test_deserialize_invalid_account_id_length() {
let mut serialized_tx = vec![];
serialized_tx.extend_from_slice(&100u32.to_le_bytes()); serialized_tx.extend_from_slice(&[b'a'; 100]);
let result = Transaction::try_from_slice(&serialized_tx);
let err = result.unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
assert!(err.to_string().contains("the Account ID is too long"));
}
#[test]
fn test_deserialize_invalid_version_tag() {
let serialized_tx = vec![2, 5, 0, 0, 0, 0, 0, 0];
let result = Transaction::try_from_slice(&serialized_tx);
let err = result.unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidData);
assert!(err.to_string().contains("invalid transaction version tag: 2"));
}
#[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: Gas::from_gas(123),
compute_usage: Some(456),
tokens_burnt: Balance::from_yoctonear(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()
);
}
}