Skip to content
This repository has been archived by the owner on Jan 11, 2024. It is now read-only.

FM-34: Query #35

Merged
merged 10 commits into from
Feb 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

118 changes: 103 additions & 15 deletions fendermint/app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ use async_trait::async_trait;
use cid::Cid;
use fendermint_abci::Application;
use fendermint_storage::{Codec, Encode, KVRead, KVReadable, KVStore, KVWritable, KVWrite};
use fendermint_vm_interpreter::bytes::{BytesMessageApplyRet, BytesMessageCheckRet};
use fendermint_vm_interpreter::bytes::{
BytesMessageApplyRet, BytesMessageCheckRet, BytesMessageQuery, BytesMessageQueryRet,
};
use fendermint_vm_interpreter::chain::{ChainMessageApplyRet, IllegalMessage};
use fendermint_vm_interpreter::fvm::{FvmApplyRet, FvmCheckRet, FvmCheckState, FvmState};
use fendermint_vm_interpreter::fvm::{
FvmApplyRet, FvmCheckRet, FvmCheckState, FvmQueryRet, FvmQueryState, FvmState,
};
use fendermint_vm_interpreter::signed::InvalidSignature;
use fendermint_vm_interpreter::{CheckInterpreter, Interpreter, Timestamp};
use fendermint_vm_interpreter::{CheckInterpreter, Interpreter, QueryInterpreter, Timestamp};
use fvm_ipld_blockstore::Blockstore;
use fvm_shared::econ::TokenAmount;
use fvm_shared::error::ExitCode;
Expand All @@ -24,6 +28,14 @@ use tendermint::abci::{request, response, Code, Event, EventAttribute};

const VERSION: &str = env!("CARGO_PKG_VERSION");

/// IPLD encoding of data types we know we must be able to encode.
macro_rules! must_encode {
($var:ident) => {
fvm_ipld_encoding::to_vec(&$var)
.unwrap_or_else(|e| panic!("error encoding {}: {}", stringify!($var), e))
};
}

#[derive(Serialize)]
#[repr(u8)]
pub enum AppStoreKey {
Expand Down Expand Up @@ -87,8 +99,12 @@ where
impl<DB, S, I> App<DB, S, I>
where
S: KVStore + Codec<AppState> + Encode<AppStoreKey>,
DB: Blockstore + KVWritable<S> + KVReadable<S> + 'static,
DB: Blockstore + KVWritable<S> + KVReadable<S> + 'static + Clone,
{
/// Get an owned clone of the database.
fn clone_db(&self) -> DB {
self.db.as_ref().clone()
}
/// Get the last committed state.
fn committed_state(&self) -> AppState {
let tx = self.db.read();
Expand Down Expand Up @@ -153,14 +169,22 @@ where
Message = Vec<u8>,
Output = BytesMessageCheckRet,
>,
I: QueryInterpreter<
State = FvmQueryState<DB>,
Query = BytesMessageQuery,
Output = BytesMessageQueryRet,
>,
{
/// Provide information about the ABCI application.
async fn info(&self, _request: request::Info) -> response::Info {
let state = self.committed_state();

let height =
tendermint::block::Height::try_from(state.block_height).expect("height too big");

let app_hash = tendermint::hash::AppHash::try_from(state.state_root.to_bytes())
.expect("hash can be wrapped");

response::Info {
data: "fendermint".to_string(),
version: VERSION.to_owned(),
Expand All @@ -176,8 +200,24 @@ where
}

/// Query the application for data at the current or past height.
async fn query(&self, _request: request::Query) -> response::Query {
todo!("make a query interpreter")
async fn query(&self, request: request::Query) -> response::Query {
let db = self.clone_db();
// TODO: Store the state for each height, or the last N heights, then use `request.height`.
let state = self.committed_state();
let block_height = state.block_height;
let state = FvmQueryState::new(db, state.state_root).expect("error creating query state");
let qry = (request.path, request.data.to_vec());

let (_, result) = self
.interpreter
.query(state, qry)
.await
.expect("error running query");

match result {
Err(e) => invalid_query(AppError::InvalidEncoding, e.description),
Ok(result) => to_query(result, block_height),
}
}

/// Check the given transaction before putting it into the local mempool.
Expand All @@ -186,16 +226,18 @@ where
let mut guard = self.check_state.lock().await;

let state = guard.take().unwrap_or_else(|| {
let db = self.db.as_ref().to_owned();
let db = self.clone_db();
let state = self.committed_state();
FvmCheckState::new(db, state.state_root).expect("error creating check state")
});

// TODO: We can make use of `request.kind` to skip signature checks on repeated calls.
let is_recheck = request.kind == CheckTxKind::Recheck;
let (state, result) = self
.interpreter
.check(state, request.tx.to_vec(), is_recheck)
.check(
state,
request.tx.to_vec(),
request.kind == CheckTxKind::Recheck,
)
.await
.expect("error running check");

Expand All @@ -216,7 +258,7 @@ where

/// Signals the beginning of a new block, prior to any `DeliverTx` calls.
async fn begin_block(&self, request: request::BeginBlock) -> response::BeginBlock {
let db = self.db.as_ref().to_owned();
let db = self.clone_db();
let state = self.committed_state();
let height = request.header.height.into();
let timestamp = Timestamp(
Expand Down Expand Up @@ -282,10 +324,12 @@ where
/// Commit the current state at the current height.
async fn commit(&self) -> response::Commit {
let exec_state = self.take_exec_state();
let block_height = exec_state.block_height();
let state_root = exec_state.commit().expect("failed to commit FVM");

let mut state = self.committed_state();
state.state_root = state_root;
state.block_height = block_height.try_into().expect("negative height");
self.set_committed_state(state);

// Reset check state.
Expand All @@ -310,7 +354,7 @@ fn invalid_deliver_tx(err: AppError, description: String) -> response::DeliverTx
}
}

/// Response to check where the input was blatantly invalid.
/// Response to checks where the input was blatantly invalid.
/// This indicates that the user who sent the transaction is either attacking or has a faulty client.
fn invalid_check_tx(err: AppError, description: String) -> response::CheckTx {
response::CheckTx {
Expand All @@ -320,6 +364,15 @@ fn invalid_check_tx(err: AppError, description: String) -> response::CheckTx {
}
}

/// Response to queries where the input was blatantly invalid.
fn invalid_query(err: AppError, description: String) -> response::Query {
response::Query {
code: Code::Err(NonZeroU32::try_from(err as u32).expect("error codes are non-zero")),
info: description,
..Default::default()
}
}

fn to_deliver_tx(ret: FvmApplyRet) -> response::DeliverTx {
let receipt = ret.apply_ret.msg_receipt;
let code = to_code(receipt.exit_code);
Expand All @@ -328,8 +381,8 @@ fn to_deliver_tx(ret: FvmApplyRet) -> response::DeliverTx {
// gas_cost = gas_fee_cap * gas_limit; this is how much the account is charged up front.
// &base_fee_burn + &over_estimation_burn + &refund + &miner_tip == gas_cost
// But that's in tokens. I guess the closes to what we want is the limit.
let gas_wanted: i64 = ret.gas_limit.try_into().expect("gas wanted not i64");
let gas_used: i64 = receipt.gas_used.try_into().expect("gas used not i64");
let gas_wanted: i64 = ret.gas_limit.try_into().unwrap_or(i64::MAX);
let gas_used: i64 = receipt.gas_used.try_into().unwrap_or(i64::MAX);

let data = receipt.return_data.to_vec().into();
let events = to_events("message", ret.apply_ret.events);
Expand All @@ -349,7 +402,7 @@ fn to_deliver_tx(ret: FvmApplyRet) -> response::DeliverTx {
fn to_check_tx(ret: FvmCheckRet) -> response::CheckTx {
response::CheckTx {
code: to_code(ret.exit_code),
gas_wanted: ret.gas_limit.try_into().expect("gas wanted not i64"),
gas_wanted: ret.gas_limit.try_into().unwrap_or(i64::MAX),
sender: ret.sender.to_string(),
..Default::default()
}
Expand Down Expand Up @@ -408,3 +461,38 @@ fn to_events(kind: &str, stamped_events: Vec<StampedEvent>) -> Vec<Event> {
})
.collect()
}

/// Map to query results.
fn to_query(ret: FvmQueryRet, block_height: u64) -> response::Query {
let exit_code = match ret {
FvmQueryRet::Ipld(None) | FvmQueryRet::ActorState(None) => ExitCode::USR_NOT_FOUND,
FvmQueryRet::Ipld(_) | FvmQueryRet::ActorState(_) => ExitCode::OK,
};

// The return value has a `key` field which is supposed to be set to the data matched.
// Although at this point I don't have access to the input like the CID looked up,
// but I assume the query sender has. Rather than repeat everything, I'll add the key
// where it gives some extra information, like the actor ID, just to keep this option visible.
let (key, value) = match ret {
FvmQueryRet::Ipld(None) | FvmQueryRet::ActorState(None) => (Vec::new(), Vec::new()),
FvmQueryRet::Ipld(Some(bz)) => (Vec::new(), bz),
FvmQueryRet::ActorState(Some(x)) => {
let (id, st) = *x;
let k = must_encode!(id);
let v = must_encode!(st);
(k, v)
}
};

// The height here is the height of the block that was committed, not in which the app hash appeared,
// so according to Tendermint docstrings we need to return plus one.
let height = tendermint::block::Height::try_from(block_height + 1).expect("height too big");

response::Query {
code: to_code(exit_code),
key: key.into(),
value: value.into(),
height,
..Default::default()
}
}
1 change: 1 addition & 0 deletions fendermint/vm/interpreter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ fendermint_vm_actor_interface = { path = "../actor_interface" }

async-trait = { workspace = true }
anyhow = { workspace = true }
serde = { workspace = true }

cid = { workspace = true }
fvm = { workspace = true }
Expand Down
45 changes: 44 additions & 1 deletion fendermint/vm/interpreter/src/bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@
// SPDX-License-Identifier: Apache-2.0, MIT
use async_trait::async_trait;

use cid::Cid;
use fendermint_vm_message::chain::ChainMessage;

use crate::{
chain::{ChainMessageApplyRet, ChainMessageCheckRet},
CheckInterpreter, Interpreter,
fvm::{FvmQuery, FvmQueryRet},
CheckInterpreter, Interpreter, QueryInterpreter,
};

pub type BytesMessageApplyRet = Result<ChainMessageApplyRet, fvm_ipld_encoding::Error>;
pub type BytesMessageCheckRet = Result<ChainMessageCheckRet, fvm_ipld_encoding::Error>;
pub type BytesMessageQueryRet = Result<FvmQueryRet, fvm_ipld_encoding::Error>;

/// Close to what the ABCI sends: (Path, Bytes).
pub type BytesMessageQuery = (String, Vec<u8>);

/// Interpreter working on raw bytes.
#[derive(Clone)]
Expand Down Expand Up @@ -92,3 +98,40 @@ where
}
}
}

#[async_trait]
impl<I> QueryInterpreter for BytesMessageInterpreter<I>
where
I: QueryInterpreter<Query = FvmQuery, Output = FvmQueryRet>,
{
type State = I::State;
type Query = BytesMessageQuery;
type Output = BytesMessageQueryRet;

async fn query(
&self,
state: Self::State,
qry: Self::Query,
) -> anyhow::Result<(Self::State, Self::Output)> {
let (path, bz) = qry;
let qry = if path.as_str() == "/store" {
// According to the docstrings, the application MUST interpret `/store` as a query on the underlying KV store.
match fvm_ipld_encoding::from_slice::<Cid>(&bz) {
Err(e) => return Ok((state, Err(e))),
Ok(cid) => FvmQuery::Ipld(cid),
}
} else {
// Otherwise ignore the path for now. The docs also say that the query bytes can be used in lieu of the path,
// so it's okay to have two ways to send IPLD queries: either by using the `/store` path and sending a CID,
// or by sending the appropriate `FvmQuery`.
match fvm_ipld_encoding::from_slice::<FvmQuery>(&bz) {
Err(e) => return Ok((state, Err(e))),
Ok(qry) => qry,
}
};

let (state, ret) = self.inner.query(state, qry).await?;

Ok((state, Ok(ret)))
}
}
20 changes: 19 additions & 1 deletion fendermint/vm/interpreter/src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use fendermint_vm_message::{chain::ChainMessage, signed::SignedMessage};

use crate::{
signed::{SignedMessageApplyRet, SignedMessageCheckRet},
CheckInterpreter, Interpreter,
CheckInterpreter, Interpreter, QueryInterpreter,
};

/// A message a user is not supposed to send.
Expand Down Expand Up @@ -102,3 +102,21 @@ where
}
}
}

#[async_trait]
impl<I> QueryInterpreter for ChainMessageInterpreter<I>
where
I: QueryInterpreter,
{
type State = I::State;
type Query = I::Query;
type Output = I::Output;

async fn query(
&self,
state: Self::State,
qry: Self::Query,
) -> anyhow::Result<(Self::State, Self::Output)> {
self.inner.query(state, qry).await
}
}
5 changes: 4 additions & 1 deletion fendermint/vm/interpreter/src/fvm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ use std::marker::PhantomData;
mod check;
mod exec;
mod externs;
mod query;
mod state;

pub use check::FvmCheckRet;
pub use exec::FvmApplyRet;
pub use state::{FvmCheckState, FvmState};
pub use fendermint_vm_message::query::FvmQuery;
pub use query::FvmQueryRet;
pub use state::{FvmCheckState, FvmQueryState, FvmState};

pub type FvmMessage = fvm_shared::message::Message;

Expand Down
45 changes: 45 additions & 0 deletions fendermint/vm/interpreter/src/fvm/query.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2022-2023 Protocol Labs
// SPDX-License-Identifier: Apache-2.0, MIT
use async_trait::async_trait;
use fendermint_vm_message::query::{ActorState, FvmQuery};
use fvm_ipld_blockstore::Blockstore;
use fvm_shared::ActorID;

use crate::QueryInterpreter;

use super::{state::FvmQueryState, FvmMessageInterpreter};

/// Internal return type for queries. It will never be serialized
/// and sent over the wire as it is, only its internal parts are
/// sent in the response. The client has to know what to expect,
/// depending on the kind of query it sent.
pub enum FvmQueryRet {
/// Bytes from the IPLD store retult, if found.
Ipld(Option<Vec<u8>>),
/// The full state of an actor, if found.
ActorState(Option<Box<(ActorID, ActorState)>>),
}

#[async_trait]
impl<DB> QueryInterpreter for FvmMessageInterpreter<DB>
where
DB: Blockstore + 'static + Send + Sync + Clone,
{
type State = FvmQueryState<DB>;
type Query = FvmQuery;
type Output = FvmQueryRet;

async fn query(
&self,
state: Self::State,
qry: Self::Query,
) -> anyhow::Result<(Self::State, Self::Output)> {
let res = match qry {
FvmQuery::Ipld(cid) => FvmQueryRet::Ipld(state.store_get(&cid)?),
FvmQuery::ActorState(addr) => {
FvmQueryRet::ActorState(state.actor_state(&addr)?.map(Box::new))
}
};
Ok((state, res))
}
}
Loading