diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index f6eb76778b..58d5230629 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -44,7 +44,7 @@ use crate::client::tendermint_rpc_types::TxResponse; /// Query the epoch of the last committed block pub async fn query_epoch(args: args::Query) -> Epoch { let client = HttpClient::new(args.ledger_address).unwrap(); - let epoch = unwrap_client_response(RPC.epoch(&client).await); + let epoch = unwrap_client_response(RPC.shell().epoch(&client).await); println!("Last committed epoch: {}", epoch); epoch } @@ -53,7 +53,7 @@ pub async fn query_epoch(args: args::Query) -> Epoch { 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.storage_value(&client, &args.storage_key).await, + RPC.shell().storage_value(&client, &args.storage_key).await, ); match bytes { Some(bytes) => println!("Found data: 0x{}", hex::encode(&bytes)), @@ -1026,7 +1026,8 @@ 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.dry_run_tx_with_options(&client, data, height, prove) + RPC.shell() + .dry_run_tx_with_options(&client, data, height, prove) .await, ) .data; @@ -1242,7 +1243,8 @@ pub async fn query_storage_value( where T: BorshDeserialize, { - let bytes = unwrap_client_response(RPC.storage_value(client, key).await); + let bytes = + unwrap_client_response(RPC.shell().storage_value(client, key).await); bytes.map(|bytes| { T::try_from_slice(&bytes[..]).unwrap_or_else(|err| { eprintln!("Error decoding the value: {}", err); @@ -1261,7 +1263,8 @@ pub async fn query_storage_prefix( where T: BorshDeserialize, { - let values = unwrap_client_response(RPC.storage_prefix(client, key).await); + let values = + unwrap_client_response(RPC.shell().storage_prefix(client, key).await); let decode = |PrefixValue { key, value }: PrefixValue| match T::try_from_slice( &value[..], @@ -1287,7 +1290,7 @@ pub async fn query_has_storage_key( client: &HttpClient, key: &storage::Key, ) -> bool { - unwrap_client_response(RPC.storage_has_key(client, key).await) + unwrap_client_response(RPC.shell().storage_has_key(client, key).await) } /// Represents a query for an event pertaining to the specified transaction diff --git a/shared/src/ledger/queries/mod.rs b/shared/src/ledger/queries/mod.rs index 4da7e1089b..0bf21490eb 100644 --- a/shared/src/ledger/queries/mod.rs +++ b/shared/src/ledger/queries/mod.rs @@ -1,7 +1,7 @@ //! Ledger read-only queries can be handled and dispatched via the [`RPC`] //! defined via `router!` macro. -use tendermint_proto::crypto::{ProofOp, ProofOps}; +use shell::{Shell, SHELL}; #[cfg(any(test, feature = "async-client"))] pub use types::Client; pub use types::{ @@ -9,35 +9,17 @@ pub use types::{ }; use super::storage::{DBIter, StorageHasher, DB}; -use super::storage_api::{self, ResultExt, StorageRead}; -use crate::types::storage::{self, Epoch, PrefixValue}; -use crate::types::transaction::TxResult; -#[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] -use crate::types::transaction::{DecryptedTx, TxType}; +use super::storage_api; #[macro_use] mod router; +mod shell; mod types; // Most commonly expected patterns should be declared first router! {RPC, - // Epoch of the last committed block - ( "epoch" ) -> Epoch = epoch, - - // Raw storage access - read value - ( "value" / [storage_key: storage::Key] ) - -> Option> = storage_value, - - // Dry run a transaction - ( "dry_run_tx" ) -> TxResult = dry_run_tx, - - // Raw storage access - prefix iterator - ( "prefix" / [storage_key: storage::Key] ) - -> Vec = storage_prefix, - - // Raw storage access - is given storage key present? - ( "has_key" / [storage_key: storage::Key] ) - -> bool = storage_has_key, + // Shell provides storage read access, block metadata and can dry-run a tx + ( "shell" ) = (sub SHELL), } /// Handle RPC query request in the ledger. On success, returns response with @@ -86,188 +68,6 @@ pub fn require_no_proof(request: &RequestQuery) -> storage_api::Result<()> { Ok(()) } -// Handlers: - -#[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] -fn dry_run_tx( - mut ctx: RequestCtx<'_, D, H>, - request: &RequestQuery, -) -> storage_api::Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - use super::gas::BlockGasMeter; - use super::storage::write_log::WriteLog; - use crate::proto::Tx; - - let mut gas_meter = BlockGasMeter::default(); - let mut write_log = WriteLog::default(); - let tx = Tx::try_from(&request.data[..]).into_storage_result()?; - let tx = TxType::Decrypted(DecryptedTx::Decrypted(tx)); - let data = super::protocol::apply_tx( - tx, - request.data.len(), - &mut gas_meter, - &mut write_log, - ctx.storage, - &mut ctx.vp_wasm_cache, - &mut ctx.tx_wasm_cache, - ) - .into_storage_result()?; - Ok(ResponseQuery { - data, - ..ResponseQuery::default() - }) -} - -#[cfg(not(all(feature = "wasm-runtime", feature = "ferveo-tpke")))] -fn dry_run_tx( - _ctx: RequestCtx<'_, D, H>, - _request: &RequestQuery, -) -> storage_api::Result> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - unimplemented!( - "dry_run_tx request handler requires \"wasm-runtime\" and \ - \"ferveo-tpke\" features enabled." - ) -} - -fn epoch( - ctx: RequestCtx<'_, D, H>, - request: &RequestQuery, -) -> 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() - }) -} - -fn storage_value( - ctx: RequestCtx<'_, D, H>, - request: &RequestQuery, - storage_key: storage::Key, -) -> storage_api::Result>>> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - match ctx - .storage - .read_with_height(&storage_key, request.height) - .into_storage_result()? - { - (Some(data), _gas) => { - let proof = if request.prove { - let proof = ctx - .storage - .get_existence_proof( - &storage_key, - data.clone(), - request.height, - ) - .into_storage_result()?; - Some(proof.into()) - } else { - None - }; - Ok(ResponseQuery { - data: Some(data), - proof_ops: proof, - ..Default::default() - }) - } - (None, _gas) => { - let proof = if request.prove { - let proof = ctx - .storage - .get_non_existence_proof(&storage_key, request.height) - .into_storage_result()?; - Some(proof.into()) - } else { - None - }; - Ok(ResponseQuery { - data: None, - proof_ops: proof, - info: format!("No value found for key: {}", storage_key), - }) - } - } -} - -fn storage_prefix( - ctx: RequestCtx<'_, D, H>, - request: &RequestQuery, - storage_key: storage::Key, -) -> storage_api::Result>> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - require_latest_height(&ctx, request)?; - - let (iter, _gas) = ctx.storage.iter_prefix(&storage_key); - let data: storage_api::Result> = iter - .map(|(key, value, _gas)| { - let key = storage::Key::parse(key).into_storage_result()?; - Ok(PrefixValue { key, value }) - }) - .collect(); - let data = data?; - let proof_ops = if request.prove { - let mut ops = vec![]; - for PrefixValue { key, value } in &data { - let proof = ctx - .storage - .get_existence_proof(key, value.clone(), request.height) - .into_storage_result()?; - let mut cur_ops: Vec = - proof.ops.into_iter().map(|op| op.into()).collect(); - ops.append(&mut cur_ops); - } - // ops is not empty in this case - Some(ProofOps { ops }) - } else { - None - }; - Ok(ResponseQuery { - data, - proof_ops, - ..Default::default() - }) -} - -fn storage_has_key( - ctx: RequestCtx<'_, D, H>, - request: &RequestQuery, - storage_key: storage::Key, -) -> 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() - }) -} - #[cfg(any(test, feature = "tendermint-rpc"))] /// Provides [`Client`] implementation for Tendermint RPC client pub mod tm { @@ -418,102 +218,3 @@ mod testing { } } } - -#[cfg(test)] -mod test { - use borsh::BorshDeserialize; - - use super::testing::TestClient; - use super::*; - use crate::ledger::storage_api::StorageWrite; - use crate::proto::Tx; - use crate::types::{address, token}; - - const TX_NO_OP_WASM: &str = "../wasm_for_tests/tx_no_op.wasm"; - - #[test] - fn test_queries_router_paths() { - let path = RPC.epoch_path(); - assert_eq!("/epoch", path); - - let token_addr = address::testing::established_address_1(); - let owner = address::testing::established_address_2(); - let key = token::balance_key(&token_addr, &owner); - let path = RPC.storage_value_path(&key); - assert_eq!(format!("/value/{}", key), path); - - let path = RPC.dry_run_tx_path(); - assert_eq!("/dry_run_tx", path); - - let path = RPC.storage_prefix_path(&key); - assert_eq!(format!("/prefix/{}", key), path); - - let path = RPC.storage_has_key_path(&key); - assert_eq!(format!("/has_key/{}", key), path); - } - - #[tokio::test] - async fn test_queries_router_with_client() -> storage_api::Result<()> { - // Initialize the `TestClient` - let mut client = TestClient::new(RPC); - - // Request last committed epoch - let read_epoch = RPC.epoch(&client).await.unwrap(); - let current_epoch = client.storage.last_epoch; - assert_eq!(current_epoch, read_epoch); - - // Request dry run tx - let tx_no_op = std::fs::read(TX_NO_OP_WASM).expect("cannot load wasm"); - let tx = Tx::new(tx_no_op, None); - let tx_bytes = tx.to_bytes(); - let result = RPC - .dry_run_tx_with_options(&client, Some(tx_bytes), None, false) - .await - .unwrap(); - assert!(result.data.is_accepted()); - - // Request storage value for a balance key ... - let token_addr = address::testing::established_address_1(); - let owner = address::testing::established_address_2(); - let balance_key = token::balance_key(&token_addr, &owner); - // ... there should be no value yet. - let read_balance = - RPC.storage_value(&client, &balance_key).await.unwrap(); - assert!(read_balance.is_none()); - - // Request storage prefix iterator - let balance_prefix = token::balance_prefix(&token_addr); - let read_balances = - RPC.storage_prefix(&client, &balance_prefix).await.unwrap(); - assert!(read_balances.is_empty()); - - // Request storage has key - let has_balance_key = - RPC.storage_has_key(&client, &balance_key).await.unwrap(); - assert!(!has_balance_key); - - // Then write some balance ... - let balance = token::Amount::from(1000); - StorageWrite::write(&mut client.storage, &balance_key, balance)?; - // ... there should be the same value now - let read_balance = - RPC.storage_value(&client, &balance_key).await.unwrap(); - assert_eq!( - balance, - token::Amount::try_from_slice(&read_balance.unwrap()).unwrap() - ); - - // Request storage prefix iterator - let balance_prefix = token::balance_prefix(&token_addr); - let read_balances = - RPC.storage_prefix(&client, &balance_prefix).await.unwrap(); - assert_eq!(read_balances.len(), 1); - - // Request storage has key - let has_balance_key = - RPC.storage_has_key(&client, &balance_key).await.unwrap(); - assert!(has_balance_key); - - Ok(()) - } -} diff --git a/shared/src/ledger/queries/router.rs b/shared/src/ledger/queries/router.rs index 69888cb935..df36167eb8 100644 --- a/shared/src/ledger/queries/router.rs +++ b/shared/src/ledger/queries/router.rs @@ -494,7 +494,7 @@ macro_rules! router_type { impl $name { #[doc = "Construct this router as a root router"] - const fn new() -> Self { + pub const fn new() -> Self { Self { prefix: String::new(), } @@ -502,7 +502,7 @@ macro_rules! router_type { #[allow(dead_code)] #[doc = "Construct this router as a sub-router at the given prefix path"] - const fn sub(prefix: String) -> Self { + pub const fn sub(prefix: String) -> Self { Self { prefix, } diff --git a/shared/src/ledger/queries/shell.rs b/shared/src/ledger/queries/shell.rs new file mode 100644 index 0000000000..2304a0421e --- /dev/null +++ b/shared/src/ledger/queries/shell.rs @@ -0,0 +1,332 @@ +use tendermint_proto::crypto::{ProofOp, ProofOps}; + +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}; +use crate::types::transaction::TxResult; +#[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] +use crate::types::transaction::{DecryptedTx, TxType}; + +router! {SHELL, + // Epoch of the last committed block + ( "epoch" ) -> Epoch = epoch, + + // Raw storage access - read value + ( "value" / [storage_key: storage::Key] ) + -> Option> = storage_value, + + // Dry run a transaction + ( "dry_run_tx" ) -> TxResult = dry_run_tx, + + // Raw storage access - prefix iterator + ( "prefix" / [storage_key: storage::Key] ) + -> Vec = storage_prefix, + + // Raw storage access - is given storage key present? + ( "has_key" / [storage_key: storage::Key] ) + -> bool = storage_has_key, +} + +// Handlers: + +#[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] +fn dry_run_tx( + mut ctx: RequestCtx<'_, D, H>, + request: &RequestQuery, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + use crate::ledger::gas::BlockGasMeter; + use crate::ledger::protocol; + use crate::ledger::storage::write_log::WriteLog; + use crate::proto::Tx; + + let mut gas_meter = BlockGasMeter::default(); + let mut write_log = WriteLog::default(); + let tx = Tx::try_from(&request.data[..]).into_storage_result()?; + let tx = TxType::Decrypted(DecryptedTx::Decrypted(tx)); + let data = protocol::apply_tx( + tx, + request.data.len(), + &mut gas_meter, + &mut write_log, + ctx.storage, + &mut ctx.vp_wasm_cache, + &mut ctx.tx_wasm_cache, + ) + .into_storage_result()?; + Ok(ResponseQuery { + data, + ..ResponseQuery::default() + }) +} + +#[cfg(not(all(feature = "wasm-runtime", feature = "ferveo-tpke")))] +fn dry_run_tx( + _ctx: RequestCtx<'_, D, H>, + _request: &RequestQuery, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + unimplemented!( + "dry_run_tx request handler requires \"wasm-runtime\" and \ + \"ferveo-tpke\" features enabled." + ) +} + +fn epoch( + ctx: RequestCtx<'_, D, H>, + request: &RequestQuery, +) -> 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() + }) +} + +fn storage_value( + ctx: RequestCtx<'_, D, H>, + request: &RequestQuery, + storage_key: storage::Key, +) -> storage_api::Result>>> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + match ctx + .storage + .read_with_height(&storage_key, request.height) + .into_storage_result()? + { + (Some(data), _gas) => { + let proof = if request.prove { + let proof = ctx + .storage + .get_existence_proof( + &storage_key, + data.clone(), + request.height, + ) + .into_storage_result()?; + Some(proof.into()) + } else { + None + }; + Ok(ResponseQuery { + data: Some(data), + proof_ops: proof, + ..Default::default() + }) + } + (None, _gas) => { + let proof = if request.prove { + let proof = ctx + .storage + .get_non_existence_proof(&storage_key, request.height) + .into_storage_result()?; + Some(proof.into()) + } else { + None + }; + Ok(ResponseQuery { + data: None, + proof_ops: proof, + info: format!("No value found for key: {}", storage_key), + }) + } + } +} + +fn storage_prefix( + ctx: RequestCtx<'_, D, H>, + request: &RequestQuery, + storage_key: storage::Key, +) -> storage_api::Result>> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + require_latest_height(&ctx, request)?; + + let (iter, _gas) = ctx.storage.iter_prefix(&storage_key); + let data: storage_api::Result> = iter + .map(|(key, value, _gas)| { + let key = storage::Key::parse(key).into_storage_result()?; + Ok(PrefixValue { key, value }) + }) + .collect(); + let data = data?; + let proof_ops = if request.prove { + let mut ops = vec![]; + for PrefixValue { key, value } in &data { + let proof = ctx + .storage + .get_existence_proof(key, value.clone(), request.height) + .into_storage_result()?; + let mut cur_ops: Vec = + proof.ops.into_iter().map(|op| op.into()).collect(); + ops.append(&mut cur_ops); + } + // ops is not empty in this case + Some(ProofOps { ops }) + } else { + None + }; + Ok(ResponseQuery { + data, + proof_ops, + ..Default::default() + }) +} + +fn storage_has_key( + ctx: RequestCtx<'_, D, H>, + request: &RequestQuery, + storage_key: storage::Key, +) -> 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() + }) +} + +#[cfg(test)] +mod test { + use borsh::BorshDeserialize; + + use crate::ledger::queries::testing::TestClient; + use crate::ledger::queries::RPC; + use crate::ledger::storage_api::{self, StorageWrite}; + use crate::proto::Tx; + use crate::types::{address, token}; + + const TX_NO_OP_WASM: &str = "../wasm_for_tests/tx_no_op.wasm"; + + #[test] + fn test_shell_queries_router_paths() { + let path = RPC.shell().epoch_path(); + assert_eq!("/shell/epoch", path); + + let token_addr = address::testing::established_address_1(); + let owner = address::testing::established_address_2(); + let key = token::balance_key(&token_addr, &owner); + let path = RPC.shell().storage_value_path(&key); + assert_eq!(format!("/shell/value/{}", key), path); + + let path = RPC.shell().dry_run_tx_path(); + assert_eq!("/shell/dry_run_tx", path); + + let path = RPC.shell().storage_prefix_path(&key); + assert_eq!(format!("/shell/prefix/{}", key), path); + + let path = RPC.shell().storage_has_key_path(&key); + assert_eq!(format!("/shell/has_key/{}", key), path); + } + + #[tokio::test] + async fn test_shell_queries_router_with_client() -> storage_api::Result<()> + { + // Initialize the `TestClient` + let mut client = TestClient::new(RPC); + + // Request last committed epoch + let read_epoch = RPC.shell().epoch(&client).await.unwrap(); + let current_epoch = client.storage.last_epoch; + assert_eq!(current_epoch, read_epoch); + + // Request dry run tx + let tx_no_op = std::fs::read(TX_NO_OP_WASM).expect("cannot load wasm"); + let tx = Tx::new(tx_no_op, None); + let tx_bytes = tx.to_bytes(); + let result = RPC + .shell() + .dry_run_tx_with_options(&client, Some(tx_bytes), None, false) + .await + .unwrap(); + assert!(result.data.is_accepted()); + + // Request storage value for a balance key ... + let token_addr = address::testing::established_address_1(); + let owner = address::testing::established_address_2(); + let balance_key = token::balance_key(&token_addr, &owner); + // ... there should be no value yet. + let read_balance = RPC + .shell() + .storage_value(&client, &balance_key) + .await + .unwrap(); + assert!(read_balance.is_none()); + + // Request storage prefix iterator + let balance_prefix = token::balance_prefix(&token_addr); + let read_balances = RPC + .shell() + .storage_prefix(&client, &balance_prefix) + .await + .unwrap(); + assert!(read_balances.is_empty()); + + // Request storage has key + let has_balance_key = RPC + .shell() + .storage_has_key(&client, &balance_key) + .await + .unwrap(); + assert!(!has_balance_key); + + // Then write some balance ... + let balance = token::Amount::from(1000); + StorageWrite::write(&mut client.storage, &balance_key, balance)?; + // ... there should be the same value now + let read_balance = RPC + .shell() + .storage_value(&client, &balance_key) + .await + .unwrap(); + assert_eq!( + balance, + token::Amount::try_from_slice(&read_balance.unwrap()).unwrap() + ); + + // Request storage prefix iterator + let balance_prefix = token::balance_prefix(&token_addr); + let read_balances = RPC + .shell() + .storage_prefix(&client, &balance_prefix) + .await + .unwrap(); + assert_eq!(read_balances.len(), 1); + + // Request storage has key + let has_balance_key = RPC + .shell() + .storage_has_key(&client, &balance_key) + .await + .unwrap(); + assert!(has_balance_key); + + Ok(()) + } +}