diff --git a/integration-tests/tests/integrations.rs b/integration-tests/tests/integrations.rs index 9d3de59..1d0fef2 100644 --- a/integration-tests/tests/integrations.rs +++ b/integration-tests/tests/integrations.rs @@ -51,6 +51,9 @@ mod wallet { test_manager.regtest_manager.generate_n_blocks(1).unwrap(); zingo_client.do_sync(false).await.unwrap(); + + // std::thread::sleep(std::time::Duration::from_secs(10)); + zingo_client .do_send(vec![( &zingolib::get_base_address!(zingo_client, "sapling"), diff --git a/zingo-rpc/src/blockcache/block.rs b/zingo-rpc/src/blockcache/block.rs index c2c2bce..de3c3f8 100644 --- a/zingo-rpc/src/blockcache/block.rs +++ b/zingo-rpc/src/blockcache/block.rs @@ -1,12 +1,17 @@ //! Block fetching and deserialization functionality. -use crate::blockcache::{ - transaction::FullTransaction, - utils::{read_bytes, read_i32, read_u32, read_zcash_script_i64, ParseError, ParseFromSlice}, +use crate::{ + blockcache::{ + transaction::FullTransaction, + utils::{ + read_bytes, read_i32, read_u32, read_zcash_script_i64, ParseError, ParseFromSlice, + }, + }, + jsonrpc::{connector::JsonRpcConnector, primitives::GetBlockResponse}, }; use sha2::{Digest, Sha256}; use std::io::Cursor; -use zcash_client_backend::proto::compact_formats::CompactBlock; +use zcash_client_backend::proto::compact_formats::{ChainMetadata, CompactBlock}; use zcash_encoding::CompactSize; /// A block header, containing metadata about a block. @@ -90,7 +95,10 @@ pub struct BlockHeaderData { } impl ParseFromSlice for BlockHeaderData { - fn parse_from_slice(data: &[u8], txid: Option>) -> Result<(&[u8], Self), ParseError> { + fn parse_from_slice( + data: &[u8], + txid: Option>>, + ) -> Result<(&[u8], Self), ParseError> { if txid != None { return Err(ParseError::InvalidData( "txid must be None for BlockHeaderData::parse_from_slice".to_string(), @@ -168,7 +176,7 @@ impl BlockHeaderData { } /// Extracts the block hash from the block header. - pub fn get_block_hash(&self) -> Result, ParseError> { + pub fn get_hash(&self) -> Result, ParseError> { let serialized_header = self.to_binary()?; let mut hasher = Sha256::new(); @@ -207,7 +215,10 @@ pub struct FullBlock { } impl ParseFromSlice for FullBlock { - fn parse_from_slice(data: &[u8], txid: Option>) -> Result<(&[u8], Self), ParseError> { + fn parse_from_slice( + data: &[u8], + txid: Option>>, + ) -> Result<(&[u8], Self), ParseError> { let txid = txid.ok_or_else(|| { ParseError::InvalidData("txid must be used for FullBlock::parse_from_slice".to_string()) })?; @@ -217,6 +228,8 @@ impl ParseFromSlice for FullBlock { BlockHeaderData::parse_from_slice(&data[cursor.position() as usize..], None)?; cursor.set_position(data.len() as u64 - remaining_data.len() as u64); + println!("\nBlockHeaderData: {:?}\n", block_header_data); + let tx_count = CompactSize::read(&mut cursor)?; if txid.len() != tx_count as usize { return Err(ParseError::InvalidData(format!( @@ -240,8 +253,10 @@ impl ParseFromSlice for FullBlock { transactions.push(tx); remaining_data = new_remaining_data; } + println!("\nTransactions: {:?}\n", transactions); + let block_height = Self::get_block_height(&transactions)?; - let block_hash = block_header_data.get_block_hash()?; + let block_hash = block_header_data.get_hash()?; Ok(( remaining_data, @@ -259,7 +274,7 @@ impl ParseFromSlice for FullBlock { /// Genesis block special case. /// -/// From LoightWalletD: +/// From LightWalletD: /// see https://github.com/zcash/lightwalletd/issues/17#issuecomment-467110828. const GENESIS_TARGET_DIFFICULTY: u32 = 520617983; @@ -286,21 +301,63 @@ impl FullBlock { } /// Decodes a hex encoded zcash full block into a FullBlock struct. - pub fn parse_full_block(data: &[u8], txid: Option>) -> Result { - todo!() + pub fn parse_full_block(data: &[u8], txid: Option>>) -> Result { + let (remaining_data, full_block) = Self::parse_from_slice(data, txid)?; + if remaining_data.len() != 0 { + return Err(ParseError::InvalidData( + "Error decoding full block, data ramaining: ({remaining_data})".to_string(), + )); + } + Ok(full_block) } /// Converts a zcash full block into a compact block. - pub fn to_compact(self) -> Result { - todo!() + pub fn to_compact( + self, + sapling_commitment_tree_size: u32, + orchard_commitment_tree_size: u32, + ) -> Result { + let vtx = self + .vtx + .into_iter() + .enumerate() + .filter_map(|(index, tx)| { + if tx.has_shielded_elements() { + Some(tx.to_compact(index as u64)) + } else { + None + } + }) + .collect::, _>>()?; + + let header = self.hdr.raw_block_header.to_binary()?; + + let compact_block = CompactBlock { + proto_version: 1, // TODO: check this is correct! + height: self.height as u64, + hash: self.hdr.cached_hash.clone(), + prev_hash: self.hdr.raw_block_header.hash_prev_block.clone(), + time: self.hdr.raw_block_header.time, + header, + vtx, + chain_metadata: Some(ChainMetadata { + sapling_commitment_tree_size, + orchard_commitment_tree_size, + }), + }; + + Ok(compact_block) } /// Decodes a hex encoded zcash full block into a CompactBlock struct. pub fn parse_to_compact( data: &[u8], - txid: Option>, + txid: Option>>, + sapling_commitment_tree_size: u32, + orchard_commitment_tree_size: u32, ) -> Result { - todo!() + Ok(Self::parse_full_block(data, txid)? + .to_compact(sapling_commitment_tree_size, orchard_commitment_tree_size)?) } } @@ -309,6 +366,61 @@ impl FullBlock { /// Retrieves a full block from zebrad/zcashd using 2 get_block calls. /// This is because a get_block verbose = 1 call is require to fetch txids. /// TODO: Save retrieved CompactBlock to the BlockCache. -pub fn get_block_from_node(height: usize) -> Result { - todo!() +/// TODO: Return more representative error type. +pub async fn get_block_from_node( + zebra_uri: &http::Uri, + height: &u32, +) -> Result { + let zebrad_client = JsonRpcConnector::new( + zebra_uri.clone(), + Some("xxxxxx".to_string()), + Some("xxxxxx".to_string()), + ) + .await; + let block_1 = zebrad_client.get_block(height.to_string(), Some(1)).await; + match block_1 { + Ok(GetBlockResponse::Object { + hash, + confirmations: _, + height: _, + time: _, + tx, + trees, + }) => { + let block_0 = zebrad_client.get_block(hash.0.to_string(), Some(0)).await; + match block_0 { + Ok(GetBlockResponse::Object { + hash: _, + confirmations: _, + height: _, + time: _, + tx: _, + trees: _, + }) => { + return Err(ParseError::InvalidData( + "Received object block type, this should not be possible here.".to_string(), + )); + } + Ok(GetBlockResponse::Raw(block_hex)) => { + Ok(FullBlock::parse_to_compact( + block_hex.as_ref(), + Some(tx.into_iter().map(|s| s.into_bytes()).collect()), + 0, //trees.sapling as u32, + 2, //trees.orchard as u32, + )?) + } + Err(e) => { + return Err(e.into()); + } + } + } + Ok(GetBlockResponse::Raw(_)) => { + return Err(ParseError::InvalidData( + "Received raw block type, this should not be possible here.".to_string(), + )); + } + Err(e) => { + return Err(e.into()); + } + } } diff --git a/zingo-rpc/src/blockcache/transaction.rs b/zingo-rpc/src/blockcache/transaction.rs index b8eaf90..d9c88f1 100644 --- a/zingo-rpc/src/blockcache/transaction.rs +++ b/zingo-rpc/src/blockcache/transaction.rs @@ -4,7 +4,9 @@ use crate::blockcache::utils::{ read_bytes, read_u32, read_u64, skip_bytes, ParseError, ParseFromSlice, }; use std::io::Cursor; -use zcash_client_backend::proto::compact_formats::{CompactBlock, CompactTx}; +use zcash_client_backend::proto::compact_formats::{ + CompactOrchardAction, CompactSaplingOutput, CompactSaplingSpend, CompactTx, +}; use zcash_encoding::CompactSize; /// Txin format as described in https://en.bitcoin.it/wiki/Transaction @@ -20,7 +22,10 @@ pub struct TxIn { } impl ParseFromSlice for TxIn { - fn parse_from_slice(data: &[u8], txid: Option>) -> Result<(&[u8], Self), ParseError> { + fn parse_from_slice( + data: &[u8], + txid: Option>>, + ) -> Result<(&[u8], Self), ParseError> { if txid != None { return Err(ParseError::InvalidData( "txid must be None for TxIn::parse_from_slice".to_string(), @@ -55,7 +60,10 @@ pub struct TxOut { } impl ParseFromSlice for TxOut { - fn parse_from_slice(data: &[u8], txid: Option>) -> Result<(&[u8], Self), ParseError> { + fn parse_from_slice( + data: &[u8], + txid: Option>>, + ) -> Result<(&[u8], Self), ParseError> { if txid != None { return Err(ParseError::InvalidData( "txid must be None for TxOut::parse_from_slice".to_string(), @@ -114,7 +122,10 @@ pub struct Spend { } impl ParseFromSlice for Spend { - fn parse_from_slice(data: &[u8], txid: Option>) -> Result<(&[u8], Self), ParseError> { + fn parse_from_slice( + data: &[u8], + txid: Option>>, + ) -> Result<(&[u8], Self), ParseError> { if txid != None { return Err(ParseError::InvalidData( "txid must be None for Spend::parse_from_slice".to_string(), @@ -156,7 +167,10 @@ pub struct Output { } impl ParseFromSlice for Output { - fn parse_from_slice(data: &[u8], txid: Option>) -> Result<(&[u8], Self), ParseError> { + fn parse_from_slice( + data: &[u8], + txid: Option>>, + ) -> Result<(&[u8], Self), ParseError> { if txid != None { return Err(ParseError::InvalidData( "txid must be None for Output::parse_from_slice".to_string(), @@ -202,7 +216,10 @@ pub struct JoinSplit { } impl ParseFromSlice for JoinSplit { - fn parse_from_slice(data: &[u8], txid: Option>) -> Result<(&[u8], Self), ParseError> { + fn parse_from_slice( + data: &[u8], + txid: Option>>, + ) -> Result<(&[u8], Self), ParseError> { if txid != None { return Err(ParseError::InvalidData( "txid must be None for JoinSplit::parse_from_slice".to_string(), @@ -254,7 +271,10 @@ pub struct Action { } impl ParseFromSlice for Action { - fn parse_from_slice(data: &[u8], txid: Option>) -> Result<(&[u8], Self), ParseError> { + fn parse_from_slice( + data: &[u8], + txid: Option>>, + ) -> Result<(&[u8], Self), ParseError> { if txid != None { return Err(ParseError::InvalidData( "txid must be None for Action::parse_from_slice".to_string(), @@ -604,7 +624,10 @@ pub struct FullTransaction { } impl ParseFromSlice for FullTransaction { - fn parse_from_slice(data: &[u8], txid: Option>) -> Result<(&[u8], Self), ParseError> { + fn parse_from_slice( + data: &[u8], + txid: Option>>, + ) -> Result<(&[u8], Self), ParseError> { let txid = txid.ok_or_else(|| { ParseError::InvalidData( "txid must be used for FullTransaction::parse_from_slice".to_string(), @@ -648,7 +671,7 @@ impl ParseFromSlice for FullTransaction { let full_transaction = FullTransaction { raw_transaction: transaction_data, raw_bytes: data[..(data.len() - remaining_data.len())].to_vec(), - tx_id: txid, + tx_id: txid[0].clone(), }; Ok((remaining_data, full_transaction)) @@ -657,7 +680,58 @@ impl ParseFromSlice for FullTransaction { impl FullTransaction { /// Converts a zcash full transaction into a compact transaction. - pub fn to_compact(self) -> Result { - todo!() + pub fn to_compact(self, index: u64) -> Result { + let hash = self.tx_id; + + // NOTE: LightWalletD currently does not return a fee and is not currently priority here. Please open an Issue or PR at the Zingo-Proxy github (https://github.com/zingolabs/zingo-proxy) if you require this functionality. + let fee = 0; + + let spends = self + .raw_transaction + .shielded_spends + .iter() + .map(|spend| CompactSaplingSpend { + nf: spend.nullifier.clone(), + }) + .collect(); + + let outputs = self + .raw_transaction + .shielded_outputs + .iter() + .map(|output| CompactSaplingOutput { + cmu: output.cmu.clone(), + ephemeral_key: output.ephemeral_key.clone(), + ciphertext: output.enc_ciphertext[..52].to_vec(), + }) + .collect(); + + let actions = self + .raw_transaction + .orchard_actions + .iter() + .map(|action| CompactOrchardAction { + nullifier: action.nullifier.clone(), + cmx: action.cmx.clone(), + ephemeral_key: action.ephemeral_key.clone(), + ciphertext: action.enc_ciphertext[..52].to_vec(), + }) + .collect(); + + Ok(CompactTx { + index, + hash, + fee, + spends, + outputs, + actions, + }) + } + + /// Returns true if the transaction contains either sapling spends or outputs. + pub fn has_shielded_elements(&self) -> bool { + !self.raw_transaction.shielded_spends.is_empty() + || !self.raw_transaction.shielded_outputs.is_empty() + || !self.raw_transaction.orchard_actions.is_empty() } } diff --git a/zingo-rpc/src/blockcache/utils.rs b/zingo-rpc/src/blockcache/utils.rs index eb9e49b..bf9b693 100644 --- a/zingo-rpc/src/blockcache/utils.rs +++ b/zingo-rpc/src/blockcache/utils.rs @@ -3,12 +3,14 @@ use byteorder::{LittleEndian, ReadBytesExt}; use std::io::{Cursor, Read}; +use crate::jsonrpc::connector::JsonRpcConnectorError; + /// Parser Error Type. #[derive(Debug)] pub enum ParseError { /// Io Error Io(std::io::Error), - /// Parse Error. + /// Invalid Data Error. InvalidData(String), } @@ -18,12 +20,39 @@ impl From for ParseError { } } +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParseError::Io(err) => write!(f, "IO Error: {}", err), + ParseError::InvalidData(msg) => write!(f, "Invalid Data Error: {}", msg), + } + } +} + +impl std::error::Error for ParseError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + ParseError::Io(err) => Some(err), + ParseError::InvalidData(_) => None, + } + } +} + +impl From for ParseError { + fn from(err: JsonRpcConnectorError) -> ParseError { + ParseError::InvalidData(err.to_string()) + } +} + /// Used for decoding zcash blocks from a bytestring. pub trait ParseFromSlice { /// Reads data from a bytestring, consuming data read, and returns an instance of self along with the remaining data in the bytestring given. /// /// Txid is givin as an input as this is taken from a get_block verbose=1 call. - fn parse_from_slice(data: &[u8], txid: Option>) -> Result<(&[u8], Self), ParseError> + fn parse_from_slice( + data: &[u8], + txid: Option>>, + ) -> Result<(&[u8], Self), ParseError> where Self: Sized; } diff --git a/zingo-rpc/src/jsonrpc/primitives.rs b/zingo-rpc/src/jsonrpc/primitives.rs index 881924b..c3f7281 100644 --- a/zingo-rpc/src/jsonrpc/primitives.rs +++ b/zingo-rpc/src/jsonrpc/primitives.rs @@ -11,7 +11,7 @@ use zebra_chain::{ transaction::{self}, transparent, }; -use zebra_rpc::methods::{GetBlockHash, GetBlockTrees}; +use zebra_rpc::methods::GetBlockHash; /// Response to a `getinfo` RPC request. /// @@ -177,6 +177,15 @@ impl AsRef<[u8]> for ProxySerializedBlock { } } +/// Information about the note commitment trees. +#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct ProxyBlockTrees { + /// Sapling commitment tree size. + pub sapling: u64, + /// Orchard commitment tree size. + pub orchard: u64, +} + /// Contains the hex-encoded hash of the sent transaction. /// /// This is used for the output parameter of [`JsonRpcConnector::get_block`]. @@ -206,7 +215,7 @@ pub enum GetBlockResponse { tx: Vec, /// Information about the note commitment trees. - trees: GetBlockTrees, + trees: zebra_rpc::methods::GetBlockTrees, }, } diff --git a/zingo-rpc/src/rpc/service.rs b/zingo-rpc/src/rpc/service.rs index 92c971e..3b87fac 100644 --- a/zingo-rpc/src/rpc/service.rs +++ b/zingo-rpc/src/rpc/service.rs @@ -12,6 +12,7 @@ use zcash_client_backend::proto::{ use zebra_chain::block::Height; use crate::{ + blockcache::block::get_block_from_node, define_grpc_passthrough, jsonrpc::{ connector::JsonRpcConnector, @@ -202,159 +203,82 @@ impl CompactTxStreamer for ProxyClient { /// Server streaming response type for the GetBlockRange method. #[doc = "Server streaming response type for the GetBlockRange method."] - // type GetBlockRangeStream = tonic::Streaming; - type GetBlockRangeStream = std::pin::Pin>; + type GetBlockRangeStream = tonic::Streaming; + // type GetBlockRangeStream = std::pin::Pin>; // /// Return a list of consecutive compact blocks. // /// // /// TODO: This implementation is slow. An internal block cache should be implemented that this rpc, along with the get_block rpc, can rely on. - // /// - Add get_block_from_node function that fetches block from full node using JsonRpcConnector and updates the block cache with this block. // /// - add get_block function that queries the block cache for block and calls get_block_from_node to fetch block if not present. - fn get_block_range<'life0, 'async_trait>( - &'life0 self, - request: tonic::Request, - ) -> core::pin::Pin< - Box< - dyn core::future::Future< - Output = std::result::Result< - tonic::Response, - tonic::Status, - >, - > + core::marker::Send - + 'async_trait, - >, - > - where - 'life0: 'async_trait, - Self: 'async_trait, - { - println!("@zingoproxyd: Received call of get_block_range."); - let zebrad_uri = self.zebrad_uri.clone(); - Box::pin(async move { - let blockrange = request.into_inner(); - let mut start = blockrange - .start - .map(|s| s.height as u32) - .ok_or(tonic::Status::invalid_argument("Start block not specified"))?; - let mut end = blockrange - .end - .map(|e| e.height as u32) - .ok_or(tonic::Status::invalid_argument("End block not specified"))?; - if start > end { - (start, end) = (end, start); - } - - let (channel_tx, channel_rx) = tokio::sync::mpsc::channel(32); - tokio::spawn(async move { - let zebrad_client = JsonRpcConnector::new( - zebrad_uri, - Some("xxxxxx".to_string()), - Some("xxxxxx".to_string()), - ) - .await; - - for height in start..end { - let block_1 = zebrad_client.get_block(height.to_string(), Some(1)).await; - match block_1 { - Ok(GetBlockResponse::Object { - hash, - confirmations: _, - height, - time, - tx, - trees, - }) => { - let block_0 = - zebrad_client.get_block(hash.0.to_string(), Some(2)).await; - match block_0 { - Ok(GetBlockResponse::Object { - hash: _, - confirmations: _, - height: _, - time: _, - tx: _, - trees: _, - }) => { - if channel_tx - .send(Err(tonic::Status::internal("Received object block type, this should not be possible here.", - ))) - .await - .is_err() - { - break; - } - } - Ok(GetBlockResponse::Raw(block_hex)) => { - let block_hash: Vec = todo!(); //block_hash_0; - let block_height: u64 = height.unwrap().0 as u64; - let block_time: u32 = time.unwrap() as u32; - let block_tx: Vec = todo!(); //tx; - let block_metadata: Option = Some(ChainMetadata { - sapling_commitment_tree_size: todo!(), //trees.sapling.size, - orchard_commitment_tree_size: todo!(), //trees.orchard.size, - }); - if channel_tx - .send(Ok(CompactBlock { - proto_version: todo!(), - height: block_height, - hash: block_hash, - prev_hash: todo!(), - time: block_time, - header: todo!(), - vtx: block_tx, - chain_metadata: block_metadata, - })) - .await - .is_err() - { - break; - } - } - Err(e) => { - if channel_tx - .send(Err(tonic::Status::internal(e.to_string()))) - .await - .is_err() - { - break; - } - } - } - } - Ok(GetBlockResponse::Raw(_)) => { - if channel_tx - .send(Err(tonic::Status::internal( - "Received raw block type, this should not be possible here.", - ))) - .await - .is_err() - { - break; - } - } - Err(e) => { - if channel_tx - .send(Err(tonic::Status::internal(e.to_string()))) - .await - .is_err() - { - break; - } - } - } - } - }); - let output_stream = CompactBlockStream::new(channel_rx); - let stream_boxed = Box::pin(output_stream); - Ok(tonic::Response::new(stream_boxed)) - }) - } - // define_grpc_passthrough!( - // fn get_block_range( - // &self, - // request: tonic::Request, - // ) -> Self::GetBlockRangeStream - // ); + // fn get_block_range<'life0, 'async_trait>( + // &'life0 self, + // request: tonic::Request, + // ) -> core::pin::Pin< + // Box< + // dyn core::future::Future< + // Output = std::result::Result< + // tonic::Response, + // tonic::Status, + // >, + // > + core::marker::Send + // + 'async_trait, + // >, + // > + // where + // 'life0: 'async_trait, + // Self: 'async_trait, + // { + // println!("@zingoproxyd: Received call of get_block_range."); + // let zebrad_uri = self.zebrad_uri.clone(); + // Box::pin(async move { + // let blockrange = request.into_inner(); + // let mut start = blockrange + // .start + // .map(|s| s.height as u32) + // .ok_or(tonic::Status::invalid_argument("Start block not specified"))?; + // let mut end = blockrange + // .end + // .map(|e| e.height as u32) + // .ok_or(tonic::Status::invalid_argument("End block not specified"))?; + // if start > end { + // (start, end) = (end, start); + // } + + // let (channel_tx, channel_rx) = tokio::sync::mpsc::channel(32); + // tokio::spawn(async move { + // for height in start..end { + // let compact_block = get_block_from_node(&zebrad_uri, &height).await; + // match compact_block { + // Ok(block) => { + // println!("\nCompact Block:\n{:?}\n", block); + + // if channel_tx.send(Ok(block)).await.is_err() { + // break; + // } + // } + // Err(e) => { + // if channel_tx + // .send(Err(tonic::Status::internal(e.to_string()))) + // .await + // .is_err() + // { + // break; + // } + // } + // } + // } + // }); + // let output_stream = CompactBlockStream::new(channel_rx); + // let stream_boxed = Box::pin(output_stream); + // Ok(tonic::Response::new(stream_boxed)) + // }) + // } + define_grpc_passthrough!( + fn get_block_range( + &self, + request: tonic::Request, + ) -> Self::GetBlockRangeStream + ); /// Server streaming response type for the GetBlockRangeNullifiers method. #[doc = " Server streaming response type for the GetBlockRangeNullifiers method."] diff --git a/zingoproxy-testutils/Cargo.toml b/zingoproxy-testutils/Cargo.toml index 78c7f3b..01e0038 100644 --- a/zingoproxy-testutils/Cargo.toml +++ b/zingoproxy-testutils/Cargo.toml @@ -24,6 +24,9 @@ tonic = { workspace = true } zingo-testutils = { git = "https://github.com/zingolabs/zingolib.git", branch = "nym_integration" } zingoconfig = { git = "https://github.com/zingolabs/zingolib.git", branch = "nym_integration" } zingolib = { git = "https://github.com/zingolabs/zingolib.git", branch = "nym_integration" } +# zingo-testutils = { path = "../../zingolib/zingo-testutils" } +# zingoconfig = { path = "../../zingolib/zingoconfig" } +# zingolib = { path = "../../zingolib/zingolib" } ctrlc = "3.2.1" tempfile = "3.2.0"