From 2c42384acc33645f9435a51256c0daefc682954e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Fri, 14 Oct 2022 16:38:56 +0200 Subject: [PATCH] router: add `with_options` for handlers that use request/response --- apps/src/lib/client/rpc.rs | 32 +++-- shared/src/ledger/queries/mod.rs | 14 +- shared/src/ledger/queries/router.rs | 198 +++++++++++++++++++++------- shared/src/ledger/queries/shell.rs | 58 +++----- 4 files changed, 205 insertions(+), 97 deletions(-) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 6bd0d6fe90d..dfa25524221 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -54,10 +54,12 @@ pub async fn query_epoch(args: args::Query) -> Epoch { /// Query the raw bytes of given storage key pub async fn query_raw_bytes(_ctx: Context, args: args::QueryRawBytes) { let client = HttpClient::new(args.query.ledger_address).unwrap(); - let bytes = unwrap_client_response( - RPC.shell().storage_value(&client, &args.storage_key).await, + let response = unwrap_client_response( + RPC.shell() + .storage_value(&client, None, None, false, &args.storage_key) + .await, ); - match bytes { + match response.data { Some(bytes) => println!("Found data: 0x{}", HEXLOWER.encode(&bytes)), None => println!("No data found for key {}", args.storage_key), } @@ -1032,9 +1034,7 @@ pub async fn dry_run_tx(ledger_address: &TendermintAddress, tx_bytes: Vec) { let client = HttpClient::new(ledger_address.clone()).unwrap(); let (data, height, prove) = (Some(tx_bytes), None, false); let result = unwrap_client_response( - RPC.shell() - .dry_run_tx_with_options(&client, data, height, prove) - .await, + RPC.shell().dry_run_tx(&client, data, height, prove).await, ) .data; println!("Dry-run result: {}", result); @@ -1249,9 +1249,12 @@ pub async fn query_storage_value( where T: BorshDeserialize, { - let bytes = - unwrap_client_response(RPC.shell().storage_value(client, key).await); - bytes.map(|bytes| { + let response = unwrap_client_response( + RPC.shell() + .storage_value(client, None, None, false, key) + .await, + ); + response.data.map(|bytes| { T::try_from_slice(&bytes[..]).unwrap_or_else(|err| { eprintln!("Error decoding the value: {}", err); cli::safe_exit(1) @@ -1269,8 +1272,11 @@ pub async fn query_storage_prefix( where T: BorshDeserialize, { - let values = - unwrap_client_response(RPC.shell().storage_prefix(client, key).await); + let values = unwrap_client_response( + RPC.shell() + .storage_prefix(client, None, None, false, key) + .await, + ); let decode = |PrefixValue { key, value }: PrefixValue| match T::try_from_slice( &value[..], @@ -1284,10 +1290,10 @@ where } Ok(value) => Some((key, value)), }; - if values.is_empty() { + if values.data.is_empty() { None } else { - Some(values.into_iter().filter_map(decode)) + Some(values.data.into_iter().filter_map(decode)) } } diff --git a/shared/src/ledger/queries/mod.rs b/shared/src/ledger/queries/mod.rs index 0bf21490ebc..b2b7ef75ba8 100644 --- a/shared/src/ledger/queries/mod.rs +++ b/shared/src/ledger/queries/mod.rs @@ -57,8 +57,7 @@ where Ok(()) } -/// For queries that only support latest height, check that the given height is -/// not different from latest height, otherwise return an error. +/// For queries that don't support proofs, require that they are not requested. pub fn require_no_proof(request: &RequestQuery) -> storage_api::Result<()> { if request.prove { return Err(storage_api::Error::new_const( @@ -68,6 +67,17 @@ pub fn require_no_proof(request: &RequestQuery) -> storage_api::Result<()> { Ok(()) } +/// For queries that don't use request data, require that there are no data +/// attached. +pub fn require_no_data(request: &RequestQuery) -> storage_api::Result<()> { + if !request.data.is_empty() { + return Err(storage_api::Error::new_const( + "This query doesn't accept request data", + )); + } + Ok(()) +} + #[cfg(any(test, feature = "tendermint-rpc"))] /// Provides [`Client`] implementation for Tendermint RPC client pub mod tm { diff --git a/shared/src/ledger/queries/router.rs b/shared/src/ledger/queries/router.rs index df36167eb87..851f48a6212 100644 --- a/shared/src/ledger/queries/router.rs +++ b/shared/src/ledger/queries/router.rs @@ -47,10 +47,10 @@ macro_rules! handle_match { return $router.internal_handle($ctx, $request, $start) }; - // Handler function + // Handler function that uses a request (`with_options`) ( $ctx:ident, $request:ident, $start:ident, $end:ident, - $handle:tt, ( $( $matched_args:ident, )* ), + (with_options $handle:tt), ( $( $matched_args:ident, )* ), ) => { // check that we're at the end of the path - trailing slash is optional if !($end == $request.path.len() || @@ -60,8 +60,6 @@ macro_rules! handle_match { println!("Not fully matched"); break } - // If you get a compile error from here with `expected function, found - // queries::Storage`, you're probably missing the marker `(sub _)` let result = $handle($ctx, $request, $( $matched_args ),* )?; let data = borsh::BorshSerialize::try_to_vec(&result.data).into_storage_result()?; return Ok($crate::ledger::queries::EncodedResponseQuery { @@ -70,6 +68,35 @@ macro_rules! handle_match { proof_ops: result.proof_ops, }); }; + + // Handler function that doesn't use the request, just the path args, if any + ( + $ctx:ident, $request:ident, $start:ident, $end:ident, + $handle:tt, ( $( $matched_args:ident, )* ), + ) => { + // check that we're at the end of the path - trailing slash is optional + if !($end == $request.path.len() || + // ignore trailing slashes + $end == $request.path.len() - 1 && &$request.path[$end..] == "/") { + // we're not at the end, no match + println!("Not fully matched"); + break + } + // Check that the request is not sent with unsupported non-default + $crate::ledger::queries::require_latest_height(&$ctx, $request)?; + $crate::ledger::queries::require_no_proof($request)?; + $crate::ledger::queries::require_no_data($request)?; + + // If you get a compile error from here with `expected function, found + // queries::Storage`, you're probably missing the marker `(sub _)` + let data = $handle($ctx, $( $matched_args ),* )?; + let data = borsh::BorshSerialize::try_to_vec(&data).into_storage_result()?; + return Ok($crate::ledger::queries::EncodedResponseQuery { + data, + info: Default::default(), + proof_ops: None, + }); + }; } /// Using TT muncher pattern on the `$tail` pattern, this macro recursively @@ -307,13 +334,12 @@ macro_rules! pattern_to_prefix { /// Turn patterns and their handlers into methods for the router, where each /// dynamic pattern is turned into a parameter for the method. macro_rules! pattern_and_handler_to_method { - // terminal rule + // terminal rule for $handle that uses request (`with_options`) ( ( $( $param:tt: $param_ty:ty ),* ) [ $( { $prefix:expr } ),* ] - // $( $return_type:path )?, $return_type:path, - $handle:tt, + (with_options $handle:tt), () ) => { // paste! used to construct the `fn $handle_path`'s name. @@ -327,29 +353,6 @@ macro_rules! pattern_and_handler_to_method { .filter_map(|x| x), "/") } - #[allow(dead_code)] - #[allow(clippy::too_many_arguments)] - #[cfg(any(test, feature = "async-client"))] - #[doc = "Request a simple borsh-encoded value from `" $handle "`, \ - without any additional request data, specified block height or \ - proof."] - pub async fn $handle(&self, client: &CLIENT, - $( $param: &$param_ty ),* - ) - -> std::result::Result< - $return_type, - ::Error - > - where CLIENT: $crate::ledger::queries::Client + std::marker::Sync { - let path = self.[<$handle _path>]( $( $param ),* ); - - let data = client.simple_request(path).await?; - - let decoded: $return_type = - borsh::BorshDeserialize::try_from_slice(&data[..])?; - Ok(decoded) - } - #[allow(dead_code)] #[allow(clippy::too_many_arguments)] #[cfg(any(test, feature = "async-client"))] @@ -357,7 +360,7 @@ macro_rules! pattern_and_handler_to_method { `dry_run_tx`), optionally specified height (supported for \ `storage_value`) and optional proof (supported for \ `storage_value` and `storage_prefix`) from `" $handle "`."] - pub async fn [<$handle _with_options>](&self, client: &CLIENT, + pub async fn $handle(&self, client: &CLIENT, data: Option>, height: Option<$crate::types::storage::BlockHeight>, prove: bool, @@ -386,6 +389,50 @@ macro_rules! pattern_and_handler_to_method { } }; + // terminal rule that $handle that doesn't use request + ( + ( $( $param:tt: $param_ty:ty ),* ) + [ $( { $prefix:expr } ),* ] + $return_type:path, + $handle:tt, + () + ) => { + // paste! used to construct the `fn $handle_path`'s name. + paste::paste! { + #[allow(dead_code)] + #[doc = "Get a path to query `" $handle "`."] + pub fn [<$handle _path>](&self, $( $param: &$param_ty ),* ) -> String { + itertools::join( + [ Some(std::borrow::Cow::from(&self.prefix)), $( $prefix ),* ] + .into_iter() + .filter_map(|x| x), "/") + } + + #[allow(dead_code)] + #[allow(clippy::too_many_arguments)] + #[cfg(any(test, feature = "async-client"))] + #[doc = "Request a simple borsh-encoded value from `" $handle "`, \ + without any additional request data, specified block height or \ + proof."] + pub async fn $handle(&self, client: &CLIENT, + $( $param: &$param_ty ),* + ) + -> std::result::Result< + $return_type, + ::Error + > + where CLIENT: $crate::ledger::queries::Client + std::marker::Sync { + let path = self.[<$handle _path>]( $( $param ),* ); + + let data = client.simple_request(path).await?; + + let decoded: $return_type = + borsh::BorshDeserialize::try_from_slice(&data[..])?; + Ok(decoded) + } + } + }; + // sub-pattern ( $param:tt @@ -580,6 +627,61 @@ macro_rules! router_type { /// methods (enabled with `feature = "async-client"`). /// /// The `router!` macro implements greedy matching algorithm. +/// +/// ## Examples +/// +/// ```rust,ignore +/// router! {ROOT, +/// // This pattern matches `/pattern_a/something`, where `something` can be +/// // parsed with `FromStr` into `ArgType`. +/// ( "pattern_a" / [typed_dynamic_arg: ArgType] ) -> ReturnType = handler, +/// +/// ( "pattern_b" / [optional_dynamic_arg: opt ArgType] ) -> ReturnType = +/// handler, +/// +/// // Untyped dynamic arg is a string slice `&str` +/// ( "pattern_c" / [untyped_dynamic_arg] ) -> ReturnType = handler, +/// +/// // The handler additionally receives the `RequestQuery`, which can have +/// // some data attached, specified block height and ask for a proof. It +/// // returns `ResponseQuery`, which can have some `info` string and a proof. +/// ( "pattern_d" ) -> ReturnType = (with_options handler), +/// +/// ( "another" / "pattern" / "that" / "goes" / "deep" ) -> ReturnType = handler, +/// +/// // Inlined sub-tree +/// ( "subtree" / [this_is_fine: ArgType] ) = { +/// ( "a" ) -> u64 = a_handler, +/// ( "b" / [another_arg] ) -> u64 = b_handler, +/// } +/// +/// // Imported sub-router - The prefix can only have literal segments +/// ( "sub" / "no_dynamic_args" ) = (sub SUB_ROUTER), +/// } +/// +/// router! {SUB_ROUTER, +/// ( "pattern" ) -> ReturnType = handler, +/// } +/// ``` +/// +/// Handler functions used in the patterns should have the expected signature: +/// ```rust,ignore +/// fn handler(ctx: RequestCtx<'_, D, H>, args ...) +/// -> storage_api::Result +/// where +/// D: 'static + DB + for<'iter> DBIter<'iter> + Sync, +/// H: 'static + StorageHasher + Sync; +/// ``` +/// +/// If the handler wants to support request options, it can be defined as +/// `(with_options $handler)` and then the expected signature is: +/// ```rust,ignore +/// fn handler(ctx: RequestCtx<'_, D, H>, request: &RequestQuery, args +/// ...) -> storage_api::Result> +/// where +/// D: 'static + DB + for<'iter> DBIter<'iter> + Sync, +/// H: 'static + StorageHasher + Sync; +/// ``` #[macro_export] macro_rules! router { { $name:ident, $( $pattern:tt $( -> $return_type:path )? = $handle:tt , )* } => ( @@ -658,9 +760,8 @@ mod test_rpc_handlers { $( pub fn $name( _ctx: RequestCtx<'_, D, H>, - _request: &RequestQuery, $( $( $param: $param_ty ),* )? - ) -> storage_api::Result> + ) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, @@ -669,10 +770,7 @@ mod test_rpc_handlers { $( $( let data = format!("{data}/{}", $param); )* )? - Ok(ResponseQuery { - data, - ..ResponseQuery::default() - }) + Ok(data) } )* }; @@ -697,11 +795,10 @@ mod test_rpc_handlers { /// support optional args. pub fn b3iii( _ctx: RequestCtx<'_, D, H>, - _request: &RequestQuery, a1: token::Amount, a2: token::Amount, a3: Option, - ) -> storage_api::Result> + ) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, @@ -710,22 +807,18 @@ mod test_rpc_handlers { let data = format!("{data}/{}", a1); let data = format!("{data}/{}", a2); let data = a3.map(|a3| format!("{data}/{}", a3)).unwrap_or(data); - Ok(ResponseQuery { - data, - ..ResponseQuery::default() - }) + Ok(data) } /// This handler is hand-written, because the test helper macro doesn't /// support optional args. pub fn b3iiii( _ctx: RequestCtx<'_, D, H>, - _request: &RequestQuery, a1: token::Amount, a2: token::Amount, a3: Option, a4: Option, - ) -> storage_api::Result> + ) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, @@ -735,6 +828,20 @@ mod test_rpc_handlers { let data = format!("{data}/{}", a2); let data = a3.map(|a3| format!("{data}/{}", a3)).unwrap_or(data); let data = a4.map(|a4| format!("{data}/{}", a4)).unwrap_or(data); + Ok(data) + } + + /// This handler is hand-written, because the test helper macro doesn't + /// support handlers with `with_options`. + pub fn c( + _ctx: RequestCtx<'_, D, H>, + _request: &RequestQuery, + ) -> storage_api::Result> + where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, + { + let data = "c".to_owned(); Ok(ResponseQuery { data, ..ResponseQuery::default() @@ -773,6 +880,7 @@ mod test_rpc { ( "iiii" / [a3: opt token::Amount] / "xyz" / [a4: opt Epoch] ) -> String = b3iiii, }, }, + ( "c" ) -> String = (with_options c), } router! {TEST_SUB_RPC, diff --git a/shared/src/ledger/queries/shell.rs b/shared/src/ledger/queries/shell.rs index 2304a0421e0..8ba800023c8 100644 --- a/shared/src/ledger/queries/shell.rs +++ b/shared/src/ledger/queries/shell.rs @@ -1,7 +1,7 @@ use tendermint_proto::crypto::{ProofOp, ProofOps}; +use crate::ledger::queries::require_latest_height; use crate::ledger::queries::types::{RequestCtx, RequestQuery, ResponseQuery}; -use crate::ledger::queries::{require_latest_height, require_no_proof}; use crate::ledger::storage::{DBIter, StorageHasher, DB}; use crate::ledger::storage_api::{self, ResultExt, StorageRead}; use crate::types::storage::{self, Epoch, PrefixValue}; @@ -15,14 +15,14 @@ router! {SHELL, // Raw storage access - read value ( "value" / [storage_key: storage::Key] ) - -> Option> = storage_value, + -> Option> = (with_options storage_value), // Dry run a transaction - ( "dry_run_tx" ) -> TxResult = dry_run_tx, + ( "dry_run_tx" ) -> TxResult = (with_options dry_run_tx), // Raw storage access - prefix iterator ( "prefix" / [storage_key: storage::Key] ) - -> Vec = storage_prefix, + -> Vec = (with_options storage_prefix), // Raw storage access - is given storage key present? ( "has_key" / [storage_key: storage::Key] ) @@ -80,22 +80,13 @@ where ) } -fn epoch( - ctx: RequestCtx<'_, D, H>, - request: &RequestQuery, -) -> storage_api::Result> +fn epoch(ctx: RequestCtx<'_, D, H>) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - require_latest_height(&ctx, request)?; - require_no_proof(request)?; - let data = ctx.storage.last_epoch; - Ok(ResponseQuery { - data, - ..Default::default() - }) + Ok(data) } fn storage_value( @@ -112,13 +103,13 @@ where .read_with_height(&storage_key, request.height) .into_storage_result()? { - (Some(data), _gas) => { + (Some(value), _gas) => { let proof = if request.prove { let proof = ctx .storage .get_existence_proof( &storage_key, - data.clone(), + value.clone().into(), request.height, ) .into_storage_result()?; @@ -127,7 +118,7 @@ where None }; Ok(ResponseQuery { - data: Some(data), + data: Some(value), proof_ops: proof, ..Default::default() }) @@ -175,7 +166,7 @@ where for PrefixValue { key, value } in &data { let proof = ctx .storage - .get_existence_proof(key, value.clone(), request.height) + .get_existence_proof(key, value.clone().into(), request.height) .into_storage_result()?; let mut cur_ops: Vec = proof.ops.into_iter().map(|op| op.into()).collect(); @@ -195,21 +186,14 @@ where fn storage_has_key( ctx: RequestCtx<'_, D, H>, - request: &RequestQuery, storage_key: storage::Key, -) -> storage_api::Result> +) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - require_latest_height(&ctx, request)?; - require_no_proof(request)?; - let data = StorageRead::has_key(ctx.storage, &storage_key)?; - Ok(ResponseQuery { - data, - ..Default::default() - }) + Ok(data) } #[cfg(test)] @@ -262,7 +246,7 @@ mod test { let tx_bytes = tx.to_bytes(); let result = RPC .shell() - .dry_run_tx_with_options(&client, Some(tx_bytes), None, false) + .dry_run_tx(&client, Some(tx_bytes), None, false) .await .unwrap(); assert!(result.data.is_accepted()); @@ -274,19 +258,19 @@ mod test { // ... there should be no value yet. let read_balance = RPC .shell() - .storage_value(&client, &balance_key) + .storage_value(&client, None, None, false, &balance_key) .await .unwrap(); - assert!(read_balance.is_none()); + assert!(read_balance.data.is_none()); // Request storage prefix iterator let balance_prefix = token::balance_prefix(&token_addr); let read_balances = RPC .shell() - .storage_prefix(&client, &balance_prefix) + .storage_prefix(&client, None, None, false, &balance_prefix) .await .unwrap(); - assert!(read_balances.is_empty()); + assert!(read_balances.data.is_empty()); // Request storage has key let has_balance_key = RPC @@ -302,22 +286,22 @@ mod test { // ... there should be the same value now let read_balance = RPC .shell() - .storage_value(&client, &balance_key) + .storage_value(&client, None, None, false, &balance_key) .await .unwrap(); assert_eq!( balance, - token::Amount::try_from_slice(&read_balance.unwrap()).unwrap() + token::Amount::try_from_slice(&read_balance.data.unwrap()).unwrap() ); // Request storage prefix iterator let balance_prefix = token::balance_prefix(&token_addr); let read_balances = RPC .shell() - .storage_prefix(&client, &balance_prefix) + .storage_prefix(&client, None, None, false, &balance_prefix) .await .unwrap(); - assert_eq!(read_balances.len(), 1); + assert_eq!(read_balances.data.len(), 1); // Request storage has key let has_balance_key = RPC