From 5a0dbe9b0c9070e13f7e1e1e2c0fc4db2a394072 Mon Sep 17 00:00:00 2001 From: Ryan Tan Date: Fri, 26 Jul 2024 15:00:36 +0100 Subject: [PATCH] feat(fa-deposit): handle fa deposit execution in proto --- crates/jstz_kernel/src/inbox.rs | 3 +- crates/jstz_mock/src/mock.rs | 22 +- crates/jstz_proto/src/context/ticket_table.rs | 18 +- crates/jstz_proto/src/error.rs | 8 +- crates/jstz_proto/src/executor/fa_deposit.rs | 418 ++++++++++++++++++ crates/jstz_proto/src/executor/mod.rs | 1 + crates/jstz_proto/src/operation.rs | 39 +- crates/jstz_proto/src/receipt.rs | 6 +- 8 files changed, 496 insertions(+), 19 deletions(-) create mode 100644 crates/jstz_proto/src/executor/fa_deposit.rs diff --git a/crates/jstz_kernel/src/inbox.rs b/crates/jstz_kernel/src/inbox.rs index 96fa82089..ddc6c6d5e 100644 --- a/crates/jstz_kernel/src/inbox.rs +++ b/crates/jstz_kernel/src/inbox.rs @@ -1,7 +1,6 @@ use jstz_crypto::public_key_hash::PublicKeyHash; use jstz_proto::operation::{external::Deposit, ExternalOperation, SignedOperation}; use num_traits::ToPrimitive; -use serde::{Deserialize, Serialize}; use tezos_crypto_rs::hash::ContractKt1Hash; use tezos_smart_rollup::inbox::ExternalMessageFrame; use tezos_smart_rollup::michelson::ticket::FA2_1Ticket; @@ -18,7 +17,7 @@ use tezos_smart_rollup::{ pub type ExternalMessage = SignedOperation; pub type InternalMessage = ExternalOperation; -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq)] pub enum Message { External(ExternalMessage), Internal(InternalMessage), diff --git a/crates/jstz_mock/src/mock.rs b/crates/jstz_mock/src/mock.rs index 91d728e6e..cad4d94b6 100644 --- a/crates/jstz_mock/src/mock.rs +++ b/crates/jstz_mock/src/mock.rs @@ -1,12 +1,12 @@ use std::io::empty; use jstz_core::{host::HostRuntime, kv::Storage}; -use jstz_crypto::hash::Blake2b; use tezos_crypto_rs::hash::ContractKt1Hash; use tezos_smart_rollup::{ michelson::{ - ticket::{FA2_1Ticket, Ticket}, + ticket::{FA2_1Ticket, Ticket, TicketHash, UnitTicket}, MichelsonBytes, MichelsonContract, MichelsonNat, MichelsonOption, MichelsonPair, + MichelsonUnit, }, storage::path::RefPath, types::{Contract, PublicKeyHash, SmartRollupAddress}, @@ -118,7 +118,19 @@ pub fn account1() -> jstz_crypto::public_key_hash::PublicKeyHash { .unwrap() } -pub fn ticket_hash1() -> Blake2b { - let data = vec![b'0', b'0', b'0']; - Blake2b::from(&data) +pub fn account2() -> jstz_crypto::public_key_hash::PublicKeyHash { + jstz_crypto::public_key_hash::PublicKeyHash::from_base58( + "tz1QcqnzZ8pa6VuE4MSeMjsJkiW94wNrPbgX", + ) + .unwrap() +} + +pub fn ticket_hash1() -> TicketHash { + let ticket = UnitTicket::new( + Contract::from_b58check("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx").unwrap(), + MichelsonUnit, + 10, + ) + .unwrap(); + ticket.hash().unwrap() } diff --git a/crates/jstz_proto/src/context/ticket_table.rs b/crates/jstz_proto/src/context/ticket_table.rs index 61041f126..48fa0b41f 100644 --- a/crates/jstz_proto/src/context/ticket_table.rs +++ b/crates/jstz_proto/src/context/ticket_table.rs @@ -1,9 +1,10 @@ use crate::error::Result; use derive_more::{Display, Error, From}; use jstz_core::kv::{Entry, Transaction}; -use jstz_crypto::{hash::Blake2b, public_key_hash::PublicKeyHash}; +use jstz_crypto::public_key_hash::PublicKeyHash; use tezos_smart_rollup::{ host::Runtime, + michelson::ticket::TicketHash, storage::path::{self, OwnedPath, RefPath}, }; @@ -21,9 +22,8 @@ const TICKET_TABLE_PATH: RefPath = RefPath::assert_from(b"/ticket_table"); pub struct TicketTable; impl TicketTable { - fn path(ticket_hash: &Blake2b, owner: &PublicKeyHash) -> Result { - let ticket_hash_path = - OwnedPath::try_from(format!("/{}", ticket_hash.to_string()))?; + fn path(ticket_hash: &TicketHash, owner: &PublicKeyHash) -> Result { + let ticket_hash_path = OwnedPath::try_from(format!("/{}", ticket_hash))?; let owner_path = OwnedPath::try_from(format!("/{}", owner))?; Ok(path::concat( @@ -36,7 +36,7 @@ impl TicketTable { rt: &mut impl Runtime, tx: &mut Transaction, owner: &PublicKeyHash, - ticket_hash: &Blake2b, + ticket_hash: &TicketHash, ) -> Result { let path = Self::path(ticket_hash, owner)?; let result = tx.get::(rt, path)?; @@ -50,7 +50,7 @@ impl TicketTable { rt: &mut impl Runtime, tx: &mut Transaction, owner: &PublicKeyHash, - ticket_hash: &Blake2b, + ticket_hash: &TicketHash, amount: Amount, // TODO: check if its the correct size ) -> Result { let path = Self::path(ticket_hash, owner)?; @@ -74,7 +74,7 @@ impl TicketTable { rt: &mut impl Runtime, tx: &mut Transaction, owner: &PublicKeyHash, - ticket_hash: &Blake2b, + ticket_hash: &TicketHash, amount: u64, ) -> Result { let path = Self::path(ticket_hash, owner)?; @@ -104,8 +104,8 @@ mod test { let ticket_hash = mock::ticket_hash1(); let owner = mock::account1(); let result = TicketTable::path(&ticket_hash, &owner).unwrap(); - let expectecd = "/ticket_table/4f3b771750d60ed12c38f5f80683fb53b37e3da02dd7381454add8f1dbd2ee60/tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx"; - assert_eq!(expectecd, result.to_string()); + let expected = "/ticket_table/4db276d5f50bc2ad959b0f08bb34fbdf4fbe4bf95a689ffb9e922038430840d7/tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx"; + assert_eq!(expected, result.to_string()); } #[test] diff --git a/crates/jstz_proto/src/error.rs b/crates/jstz_proto/src/error.rs index a45c41d15..27fdd8065 100644 --- a/crates/jstz_proto/src/error.rs +++ b/crates/jstz_proto/src/error.rs @@ -1,7 +1,7 @@ use boa_engine::{JsError, JsNativeError}; use derive_more::{Display, Error, From}; -use crate::context::ticket_table; +use crate::{context::ticket_table, executor::fa_deposit}; #[derive(Display, Debug, Error, From)] pub enum Error { @@ -20,6 +20,9 @@ pub enum Error { TicketTableError { source: ticket_table::TicketTableError, }, + FaDepositError { + source: fa_deposit::FaDepositError, + }, } pub type Result = std::result::Result; @@ -51,6 +54,9 @@ impl From for JsError { Error::TicketTableError { source } => JsNativeError::eval() .with_message(format!("TicketTableError: {}", source)) .into(), + Error::FaDepositError { source } => JsNativeError::eval() + .with_message(format!("FaDepositError: {}", source)) + .into(), } } } diff --git a/crates/jstz_proto/src/executor/fa_deposit.rs b/crates/jstz_proto/src/executor/fa_deposit.rs new file mode 100644 index 000000000..e12091c3d --- /dev/null +++ b/crates/jstz_proto/src/executor/fa_deposit.rs @@ -0,0 +1,418 @@ +use crate::{ + context::{account::Amount, ticket_table::TicketTable}, + executor::smart_function, + operation::{external::FaDeposit, RunFunction}, + receipt::Receipt, + Result, +}; +use derive_more::{Display, Error, From}; +use http::{header::CONTENT_TYPE, HeaderMap, Method, Uri}; +use jstz_api::http::body::HttpBody; +use jstz_core::{host::HostRuntime, kv::Transaction}; +use jstz_crypto::public_key_hash::PublicKeyHash; +use serde::{Deserialize, Serialize}; +use tezos_smart_rollup::{michelson::ticket::TicketHash, prelude::debug_msg}; + +const FA_DEPOSIT_GAS_LIMIT: usize = usize::MAX; + +// TODO: https://linear.app/tezos/issue/JSTZ-36/use-cryptos-from-tezos-crypto +// Properly represent the null address +const NULL_ADDRESS: &str = "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx"; +const DEPOSIT_URI: &str = "/-/deposit"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FaDepositReceiptContent { + pub receiver: PublicKeyHash, + pub ticket_balance: Amount, + pub run_function: Option, +} + +#[derive(Display, Debug, Error, From)] +pub enum FaDepositError { + InvalidHeaderValue, + InvalidUri, +} + +fn deposit_to_receiver( + rt: &mut impl HostRuntime, + tx: &mut Transaction, + receiver: &PublicKeyHash, + ticket_hash: &TicketHash, + amount: Amount, +) -> Result { + let final_balance = TicketTable::add(rt, tx, receiver, ticket_hash, amount)?; + Ok(FaDepositReceiptContent { + receiver: receiver.clone(), + ticket_balance: final_balance, + run_function: None, + }) +} + +fn new_run_function( + http_body: HttpBody, + proxy_contract: &PublicKeyHash, +) -> Result { + let mut headers = HeaderMap::new(); + headers.insert( + CONTENT_TYPE, + "application/json; charset=utf-8" + .parse() + .map_err(|_| FaDepositError::InvalidHeaderValue)?, + ); + Ok(RunFunction { + uri: Uri::builder() + .scheme("tezos") + .authority(proxy_contract.to_string()) + .path_and_query(DEPOSIT_URI) + .build() + .map_err(|_| FaDepositError::InvalidUri)?, + method: Method::POST, + headers, + body: http_body, + gas_limit: FA_DEPOSIT_GAS_LIMIT, + }) +} + +fn deposit_to_proxy_contract( + rt: &mut impl HostRuntime, + tx: &mut Transaction, + deposit: &FaDeposit, + proxy_contract: &PublicKeyHash, +) -> Result { + let run = new_run_function(deposit.to_http_body(), proxy_contract)?; + let source = PublicKeyHash::from_base58(NULL_ADDRESS)?; + let result = smart_function::run::execute(rt, tx, &source, run, deposit.hash()); + match result { + Ok(run_receipt) => { + if run_receipt.status_code.is_success() { + let final_balance = TicketTable::add( + rt, + tx, + proxy_contract, + &deposit.ticket_hash, + deposit.amount, + )?; + Ok(FaDepositReceiptContent { + receiver: proxy_contract.clone(), + ticket_balance: final_balance, + run_function: Some(run_receipt), + }) + } else { + let mut result = deposit_to_receiver( + rt, + tx, + &deposit.receiver, + &deposit.ticket_hash, + deposit.amount, + )?; + result.run_function = Some(run_receipt); + Ok(result) + } + } + Err(error) => { + debug_msg!( + rt, + "Failed to execute proxy function when performing fa deposit: {error:?}\n" + ); + let result = deposit_to_receiver( + rt, + tx, + &deposit.receiver, + &deposit.ticket_hash, + deposit.amount, + )?; + Ok(result) + } + } +} + +fn execute_inner( + rt: &mut impl HostRuntime, + tx: &mut Transaction, + deposit: &FaDeposit, +) -> Result { + match &deposit.proxy_smart_function { + None => deposit_to_receiver( + rt, + tx, + &deposit.receiver, + &deposit.ticket_hash, + deposit.amount, + ), + Some(proxy_contract) => { + deposit_to_proxy_contract(rt, tx, deposit, proxy_contract) + } + } +} + +pub fn execute( + rt: &mut impl HostRuntime, + tx: &mut Transaction, + deposit: FaDeposit, +) -> Receipt { + let content = execute_inner(rt, tx, &deposit) + .expect("Unreachable: Failed to execute fa deposit!\n"); + let operation_hash = deposit.hash(); + Receipt::new( + operation_hash, + Ok(crate::receipt::Content::FaDeposit(content)), + ) +} + +#[cfg(test)] +mod test { + + use std::io::empty; + + use jstz_core::kv::Transaction; + use jstz_crypto::public_key_hash::PublicKeyHash; + use jstz_mock::mock; + use tezos_smart_rollup_mock::MockHost; + + use crate::{ + context::{account::ParsedCode, ticket_table::TicketTable}, + executor::fa_deposit::{FaDeposit, FaDepositReceiptContent}, + receipt::{Content, Receipt}, + }; + + fn mock_fa_deposit(proxy: Option) -> FaDeposit { + FaDeposit { + inbox_id: 34, + amount: 42, + receiver: mock::account2(), + proxy_smart_function: proxy, + ticket_hash: mock::ticket_hash1(), + } + } + + #[test] + fn execute_fa_deposit_into_account_succeeds() { + let fa_deposit = mock_fa_deposit(None); + let expected_receiver = fa_deposit.receiver.clone(); + let ticket_hash = fa_deposit.ticket_hash.clone(); + let expected_balance = fa_deposit.amount; + let expected_hash = fa_deposit.hash(); + let mut host = MockHost::default(); + let mut tx = Transaction::default(); + tx.begin(); + let receipt = super::execute(&mut host, &mut tx, fa_deposit); + + assert_eq!(expected_hash, *receipt.hash()); + + match receipt.inner { + Ok(Content::FaDeposit(FaDepositReceiptContent { + receiver, + ticket_balance, + run_function, + })) => { + assert_eq!(expected_receiver, receiver); + assert_eq!(expected_balance, ticket_balance); + assert!(run_function.is_none()); + + let balance = TicketTable::get_balance( + &mut host, + &mut tx, + &expected_receiver, + &ticket_hash, + ) + .unwrap(); + assert_eq!(expected_balance, balance); + } + _ => panic!("Expected success"), + } + } + + #[test] + fn execute_multiple_fa_deposit_into_account_succeeds() { + let fa_deposit1 = mock_fa_deposit(None); + let fa_deposit2 = mock_fa_deposit(None); + let expected_receiver = fa_deposit2.receiver.clone(); + let ticket_hash = fa_deposit2.ticket_hash.clone(); + let expected_hash = fa_deposit2.hash(); + let mut host = MockHost::default(); + let mut tx = Transaction::default(); + tx.begin(); + + let _ = super::execute(&mut host, &mut tx, fa_deposit1); + let receipt = super::execute(&mut host, &mut tx, fa_deposit2); + + assert_eq!(expected_hash, *receipt.hash()); + + match receipt.inner { + Ok(Content::FaDeposit(FaDepositReceiptContent { + receiver, + ticket_balance, + run_function, + })) => { + assert_eq!(84, ticket_balance); + assert_eq!(expected_receiver, receiver); + assert!(run_function.is_none()); + let balance = TicketTable::get_balance( + &mut host, + &mut tx, + &expected_receiver, + &ticket_hash, + ) + .unwrap(); + assert_eq!(84, balance); + } + _ => panic!("Expected success"), + } + } + + #[test] + fn execute_fa_deposit_into_proxy_succeeds() { + let mut host = MockHost::default(); + host.set_debug_handler(empty()); + let mut tx = Transaction::default(); + let source = mock::account1(); + let code = r#" + export default (request) => { + const url = new URL(request.url) + if (url.pathname === "/-/deposit") { + return new Response(); + } + return Response.error(); + } + "#; + let parsed_code = ParsedCode::try_from(code.to_string()).unwrap(); + tx.begin(); + let proxy = crate::executor::smart_function::Script::deploy( + &mut host, + &mut tx, + &source, + parsed_code, + 100, + ) + .unwrap(); + let fa_deposit = mock_fa_deposit(Some(proxy.clone())); + let ticket_hash = fa_deposit.ticket_hash.clone(); + + let Receipt { inner, .. } = super::execute(&mut host, &mut tx, fa_deposit); + + match inner { + Ok(Content::FaDeposit(FaDepositReceiptContent { + receiver, + ticket_balance, + run_function, + })) => { + assert_eq!(42, ticket_balance); + assert_eq!(proxy, receiver); + assert!(run_function.is_some()); + let balance = + TicketTable::get_balance(&mut host, &mut tx, &proxy, &ticket_hash) + .unwrap(); + assert_eq!(42, balance); + } + _ => panic!("Expected success"), + } + } + + #[test] + fn execute_multiple_fa_deposit_into_proxy_succeeds() { + let mut host = MockHost::default(); + host.set_debug_handler(empty()); + let mut tx = Transaction::default(); + let source = mock::account1(); + let code = r#" + export default (request) => { + const url = new URL(request.url) + if (url.pathname === "/-/deposit") { + return new Response(); + } + return Response.error(); + } + "#; + let parsed_code = ParsedCode::try_from(code.to_string()).unwrap(); + tx.begin(); + let proxy = crate::executor::smart_function::Script::deploy( + &mut host, + &mut tx, + &source, + parsed_code, + 100, + ) + .unwrap(); + let fa_deposit1 = mock_fa_deposit(Some(proxy.clone())); + let ticket_hash = fa_deposit1.ticket_hash.clone(); + + let _ = super::execute(&mut host, &mut tx, fa_deposit1); + + let fa_deposit2 = mock_fa_deposit(Some(proxy.clone())); + + let Receipt { inner, .. } = super::execute(&mut host, &mut tx, fa_deposit2); + + match inner { + Ok(Content::FaDeposit(FaDepositReceiptContent { + receiver, + ticket_balance, + run_function, + })) => { + assert_eq!(84, ticket_balance); + assert_eq!(proxy, receiver); + assert!(run_function.is_some()); + let balance = + TicketTable::get_balance(&mut host, &mut tx, &proxy, &ticket_hash) + .unwrap(); + assert_eq!(84, balance); + } + _ => panic!("Expected success"), + } + } + + #[test] + fn execute_fa_deposit_fails_when_proxy_contract_fails() { + let mut host = MockHost::default(); + host.set_debug_handler(empty()); + let mut tx = Transaction::default(); + tx.begin(); + let source = mock::account1(); + let code = r#" + export default (request) => { + const url = new URL(request.url) + return Response.error(); + } + "#; + let parsed_code = ParsedCode::try_from(code.to_string()).unwrap(); + let proxy = crate::executor::smart_function::Script::deploy( + &mut host, + &mut tx, + &source, + parsed_code, + 100, + ) + .unwrap(); + + let fa_deposit = mock_fa_deposit(Some(proxy.clone())); + let expected_receiver = fa_deposit.receiver.clone(); + let ticket_hash = fa_deposit.ticket_hash.clone(); + + let Receipt { inner, .. } = super::execute(&mut host, &mut tx, fa_deposit); + + match inner { + Ok(Content::FaDeposit(FaDepositReceiptContent { + receiver, + ticket_balance, + run_function, + })) => { + assert_eq!(500, run_function.unwrap().status_code); + assert_eq!(expected_receiver, receiver); + assert_eq!(42, ticket_balance); + let proxy_balance = + TicketTable::get_balance(&mut host, &mut tx, &proxy, &ticket_hash) + .unwrap(); + assert_eq!(0, proxy_balance); + + let receiver_balance = TicketTable::get_balance( + &mut host, + &mut tx, + &expected_receiver, + &ticket_hash, + ) + .unwrap(); + assert_eq!(42, receiver_balance); + } + _ => panic!("Expected success"), + } + } +} diff --git a/crates/jstz_proto/src/executor/mod.rs b/crates/jstz_proto/src/executor/mod.rs index afdb97568..a8d8dcaee 100644 --- a/crates/jstz_proto/src/executor/mod.rs +++ b/crates/jstz_proto/src/executor/mod.rs @@ -7,6 +7,7 @@ use crate::{ }; pub mod deposit; +pub mod fa_deposit; pub mod smart_function; fn execute_operation_inner( diff --git a/crates/jstz_proto/src/operation.rs b/crates/jstz_proto/src/operation.rs index e208af80f..395474ab5 100644 --- a/crates/jstz_proto/src/operation.rs +++ b/crates/jstz_proto/src/operation.rs @@ -133,6 +133,8 @@ impl SignedOperation { } pub mod external { + use tezos_smart_rollup::michelson::ticket::TicketHash; + use super::*; #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -140,9 +142,44 @@ pub mod external { pub amount: Amount, pub reciever: Address, } + + #[derive(Debug, PartialEq, Eq)] + pub struct FaDeposit { + // Inbox message id is unique to each message and + // suitable as a nonce + pub inbox_id: u32, + // Amount to deposit + pub amount: Amount, + // Final deposit receiver address + pub receiver: Address, + // Optional proxy contract + pub proxy_smart_function: Option
, + // Ticket hash + pub ticket_hash: TicketHash, + } + + impl FaDeposit { + fn json(&self) -> serde_json::Value { + serde_json::json!({ + "receiver": self.receiver, + "amount": self.amount, + "ticketHash": self.ticket_hash.to_string(), + }) + } + + pub fn to_http_body(&self) -> HttpBody { + let body = self.json(); + Some(String::as_bytes(&body.to_string()).to_vec()) + } + + pub fn hash(&self) -> OperationHash { + let seed = self.inbox_id.to_be_bytes(); + Blake2b::from(seed.as_slice()) + } + } } -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq)] pub enum ExternalOperation { Deposit(external::Deposit), } diff --git a/crates/jstz_proto/src/receipt.rs b/crates/jstz_proto/src/receipt.rs index 633792766..df1c9d08c 100644 --- a/crates/jstz_proto/src/receipt.rs +++ b/crates/jstz_proto/src/receipt.rs @@ -2,7 +2,10 @@ use http::{HeaderMap, StatusCode}; use jstz_api::http::body::HttpBody; use serde::{Deserialize, Serialize}; -use crate::{context::account::Address, operation::OperationHash, Result}; +use crate::{ + context::account::Address, executor::fa_deposit::FaDepositReceiptContent, + operation::OperationHash, Result, +}; pub type ReceiptResult = std::result::Result; @@ -41,4 +44,5 @@ pub struct RunFunction { pub enum Content { DeployFunction(DeployFunction), RunFunction(RunFunction), + FaDeposit(FaDepositReceiptContent), }