diff --git a/Cargo.lock b/Cargo.lock index 6ef5357d8..a179224af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7548,6 +7548,7 @@ name = "lightning-types" version = "0.1.0" dependencies = [ "anyhow", + "atomo", "bincode", "cid 0.10.1", "compile-time-run", diff --git a/core/application/src/state/query.rs b/core/application/src/state/query.rs index b7b8754e5..1e40cccef 100644 --- a/core/application/src/state/query.rs +++ b/core/application/src/state/query.rs @@ -29,6 +29,8 @@ use lightning_interfaces::types::{ Service, ServiceId, ServiceRevenue, + StateProofKey, + StateProofValue, TotalServed, TransactionRequest, TransactionResponse, @@ -268,4 +270,24 @@ impl SyncQueryRunnerInterface for QueryRunner { fn get_state_root(&self) -> Result { self.run(|ctx| ApplicationStateTree::get_state_root(ctx)) } + + /// Returns the state proof for a given key from the application state using the state tree. + fn get_state_proof( + &self, + key: StateProofKey, + ) -> Result<( + Option, + ::Proof, + )> { + type Serde = ::Serde; + + self.run(|ctx| { + let (table, serialized_key) = key.raw::(); + let proof = ApplicationStateTree::get_state_proof(ctx, &table, serialized_key.clone())?; + let value = self + .run(|ctx| ctx.get_raw_value(table, &serialized_key)) + .map(|value| key.value::(value)); + Ok((value, proof)) + }) + } } diff --git a/core/interfaces/src/application.rs b/core/interfaces/src/application.rs index 556979cb9..345ed01e9 100644 --- a/core/interfaces/src/application.rs +++ b/core/interfaces/src/application.rs @@ -13,10 +13,13 @@ use lightning_types::{ ChainId, Committee, NodeIndex, + StateProofKey, + StateProofValue, TransactionRequest, TxHash, Value, }; +use merklize::trees::mpt::MptStateProof; use merklize::StateRootHash; use serde::{Deserialize, Serialize}; @@ -191,6 +194,12 @@ pub trait SyncQueryRunnerInterface: Clone + Send + Sync + 'static { /// Returns the state root hash from the application state. fn get_state_root(&self) -> Result; + + /// Returns the state proof for a given key from the application state using the state tree. + fn get_state_proof( + &self, + key: StateProofKey, + ) -> Result<(Option, MptStateProof)>; } #[derive(Clone, Debug)] diff --git a/core/rpc/Cargo.toml b/core/rpc/Cargo.toml index 684a4fd32..d0082890a 100644 --- a/core/rpc/Cargo.toml +++ b/core/rpc/Cargo.toml @@ -32,6 +32,7 @@ reqwest.workspace = true once_cell = "1.19" clap = { version = "4.4.10", features = ["derive"] } +lightning-application = { path = "../application" } lightning-firewall = { path = "../firewall" } lightning-types = { path = "../types" } lightning-interfaces = { path = "../interfaces" } @@ -52,7 +53,6 @@ hex = "0.4.3" [dev-dependencies] reqwest.workspace = true lightning-test-utils = { path = "../test-utils" } -lightning-application = { path = "../application" } lightning-fetcher = { path = "../fetcher" } lightning-blockstore = { path = "../blockstore" } lightning-blockstore-server = { path = "../blockstore-server" } diff --git a/core/rpc/src/api/flk.rs b/core/rpc/src/api/flk.rs index dc2b53d79..80704e115 100644 --- a/core/rpc/src/api/flk.rs +++ b/core/rpc/src/api/flk.rs @@ -4,6 +4,7 @@ use fleek_crypto::{EthAddress, NodePublicKey}; use hp_fixed::unsigned::HpUfixed; use jsonrpsee::core::{RpcResult, SubscriptionResult}; use jsonrpsee::proc_macros::rpc; +use lightning_application::env::ApplicationStateTree; use lightning_interfaces::types::{ AccountInfo, Blake3Hash, @@ -23,7 +24,8 @@ use lightning_interfaces::types::{ }; use lightning_interfaces::PagingParams; use lightning_openrpc_macros::open_rpc; -use merklize::StateRootHash; +use lightning_types::{StateProofKey, StateProofValue}; +use merklize::{StateRootHash, StateTree}; #[open_rpc(namespace = "flk", tag = "1.0.0")] #[rpc(client, server, namespace = "flk")] @@ -189,6 +191,16 @@ pub trait FleekApi { #[method(name = "get_state_root")] async fn get_state_root(&self, epoch: Option) -> RpcResult; + #[method(name = "get_state_proof")] + async fn get_state_proof( + &self, + key: StateProofKey, + epoch: Option, + ) -> RpcResult<( + Option, + ::Proof, + )>; + #[method(name = "send_txn")] async fn send_txn(&self, tx: TransactionRequest) -> RpcResult<()>; diff --git a/core/rpc/src/logic/flk_impl.rs b/core/rpc/src/logic/flk_impl.rs index ae0b1777e..416c3cce8 100644 --- a/core/rpc/src/logic/flk_impl.rs +++ b/core/rpc/src/logic/flk_impl.rs @@ -5,6 +5,7 @@ use fleek_crypto::{EthAddress, NodePublicKey}; use hp_fixed::unsigned::HpUfixed; use jsonrpsee::core::{RpcResult, SubscriptionResult}; use jsonrpsee::{PendingSubscriptionSink, SubscriptionMessage}; +use lightning_application::env::ApplicationStateTree; use lightning_interfaces::prelude::*; use lightning_interfaces::types::{ AccountInfo, @@ -29,8 +30,9 @@ use lightning_interfaces::types::{ Value, }; use lightning_interfaces::PagingParams; +use lightning_types::{StateProofKey, StateProofValue}; use lightning_utils::application::QueryRunnerExt; -use merklize::StateRootHash; +use merklize::{StateRootHash, StateTree}; use crate::api::FleekApiServer; use crate::error::RPCError; @@ -388,6 +390,7 @@ impl FleekApiServer for FleekApi { Ok((sub_dag_index, self.data.query_runner.get_epoch_info().epoch)) } + /// Returns the state root for the given epoch. async fn get_state_root(&self, epoch: Option) -> RpcResult { Ok(self .data @@ -397,6 +400,24 @@ impl FleekApiServer for FleekApi { .map_err(|e| RPCError::custom(e.to_string()))?) } + /// Returns the state proof for a given key and epoch. + async fn get_state_proof( + &self, + key: StateProofKey, + epoch: Option, + ) -> RpcResult<( + Option, + ::Proof, + )> { + let (value, proof) = self + .data + .query_runner(epoch) + .await? + .get_state_proof(key) + .map_err(|e| RPCError::custom(e.to_string()))?; + Ok((value, proof)) + } + async fn send_txn(&self, tx: TransactionRequest) -> RpcResult<()> { Ok(self .data diff --git a/core/rpc/src/tests.rs b/core/rpc/src/tests.rs index f4c6a5292..134954ad4 100644 --- a/core/rpc/src/tests.rs +++ b/core/rpc/src/tests.rs @@ -13,6 +13,7 @@ use hp_fixed::unsigned::HpUfixed; use jsonrpsee::http_client::{HttpClient, HttpClientBuilder}; use lightning_application::app::Application; use lightning_application::config::Config as AppConfig; +use lightning_application::env::ApplicationStateTree; use lightning_application::genesis::{Genesis, GenesisAccount, GenesisNode, GenesisNodeServed}; use lightning_application::state::QueryRunner; use lightning_blockstore::blockstore::Blockstore; @@ -38,8 +39,9 @@ use lightning_rep_collector::ReputationAggregator; use lightning_signer::Signer; use lightning_test_utils::json_config::JsonConfigProvider; use lightning_test_utils::keys::EphemeralKeystore; -use lightning_types::FirewallConfig; +use lightning_types::{AccountInfo, FirewallConfig, StateProofKey, StateProofValue}; use lightning_utils::application::QueryRunnerExt; +use merklize::StateProof; use reqwest::Client; use resolved_pathbuf::ResolvedPathBuf; use serde::{Deserialize, Serialize}; @@ -1181,3 +1183,68 @@ async fn test_rpc_get_state_root() -> Result<()> { node.shutdown().await; Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn test_rpc_get_state_proof() -> Result<()> { + let temp_dir = tempdir()?; + + // Create keys + let owner_secret_key = AccountOwnerSecretKey::generate(); + let owner_public_key = owner_secret_key.to_pk(); + let owner_eth_address: EthAddress = owner_public_key.into(); + + // Init application service + let mut genesis = Genesis::default(); + genesis.account.push(GenesisAccount { + public_key: owner_public_key.into(), + flk_balance: 1000u64.into(), + stables_balance: 0, + bandwidth_balance: 0, + }); + + let genesis_path = genesis + .write_to_dir(temp_dir.path().to_path_buf().try_into().unwrap()) + .unwrap(); + + let port = 30025; + let node = init_rpc(&temp_dir, genesis_path, port).await; + + wait_for_server_start(port).await?; + + let client = RpcClient::new_no_auth(&format!("http://127.0.0.1:{port}/rpc/v0"))?; + let state_key = StateProofKey::Accounts(owner_eth_address); + let (value, proof) = + FleekApiClient::get_state_proof(&client, StateProofKey::Accounts(owner_eth_address), None) + .await?; + + assert!(value.is_some()); + let value = value.unwrap(); + assert_eq!( + value.clone(), + StateProofValue::Accounts(AccountInfo { + flk_balance: 1000u64.into(), + stables_balance: HpUfixed::zero(), + bandwidth_balance: 0, + nonce: 0, + }) + ); + + // Verify proof. + let root_hash = FleekApiClient::get_state_root(&client, None).await?; + proof + .verify_membership::<_, _, ApplicationStateTree>( + state_key.table(), + owner_eth_address, + AccountInfo { + flk_balance: 1000u64.into(), + stables_balance: HpUfixed::zero(), + bandwidth_balance: 0, + nonce: 0, + }, + root_hash, + ) + .unwrap(); + + node.shutdown().await; + Ok(()) +} diff --git a/core/types/Cargo.toml b/core/types/Cargo.toml index 81ea5e170..26c37a357 100644 --- a/core/types/Cargo.toml +++ b/core/types/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] anyhow.workspace = true +atomo.workspace = true cid.workspace = true serde.workspace = true ink-quill.workspace = true diff --git a/core/types/src/lib.rs b/core/types/src/lib.rs index 0d70203fc..f2e312ffa 100644 --- a/core/types/src/lib.rs +++ b/core/types/src/lib.rs @@ -18,6 +18,7 @@ mod reputation; mod response; mod rpc; mod state; +mod state_proof; mod transaction; pub use application::*; @@ -38,6 +39,7 @@ pub use reputation::*; pub use response::*; pub use rpc::*; pub use state::*; +pub use state_proof::*; pub use transaction::*; /// The physical address of a node where it can be reached, the port numbers are diff --git a/core/types/src/state.rs b/core/types/src/state.rs index fc983fdc4..d37888007 100644 --- a/core/types/src/state.rs +++ b/core/types/src/state.rs @@ -37,13 +37,13 @@ impl Tokens { } } -#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Default, schemars::JsonSchema)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Default, schemars::JsonSchema)] pub struct NodeServed { pub served: CommodityServed, pub stables_revenue: HpUfixed<6>, } -#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Default, schemars::JsonSchema)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Default, schemars::JsonSchema)] pub struct TotalServed { pub served: CommodityServed, pub reward_pool: HpUfixed<6>, @@ -75,14 +75,14 @@ pub enum CommodityTypes { Gpu = 2, } -#[derive(Clone, Debug, Hash, Serialize, Deserialize, schemars::JsonSchema)] +#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] pub struct ReportedReputationMeasurements { pub reporting_node: NodeIndex, pub measurements: ReputationMeasurements, } /// Metadata, state stored in the blockchain that applies to the current block -#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)] pub enum Metadata { ChainId, Epoch, @@ -100,7 +100,7 @@ pub enum Metadata { } /// The Value enum is a data type used to represent values in a key-value pair for a metadata table -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] pub enum Value { ChainId(u32), Epoch(u64), @@ -378,7 +378,19 @@ pub struct Service { pub slashing: (), } -#[derive(Debug, Hash, PartialEq, PartialOrd, Ord, Eq, Serialize, Deserialize, Clone, Default)] +#[derive( + Debug, + Hash, + PartialEq, + PartialOrd, + Ord, + Eq, + Serialize, + Deserialize, + Clone, + Default, + schemars::JsonSchema, +)] pub struct Committee { pub members: Vec, pub ready_to_change: Vec, diff --git a/core/types/src/state_proof.rs b/core/types/src/state_proof.rs new file mode 100644 index 000000000..31f05906d --- /dev/null +++ b/core/types/src/state_proof.rs @@ -0,0 +1,178 @@ +use std::collections::BTreeSet; +use std::time::Duration; + +use atomo::SerdeBackend; +use fleek_crypto::{ClientPublicKey, ConsensusPublicKey, EthAddress, NodePublicKey}; +use hp_fixed::unsigned::HpUfixed; +use serde::{Deserialize, Serialize}; + +use crate::{ + AccountInfo, + Blake3Hash, + Committee, + CommodityTypes, + Epoch, + Metadata, + NodeIndex, + NodeInfo, + NodeServed, + ProtocolParams, + ReportedReputationMeasurements, + Service, + ServiceId, + ServiceRevenue, + TotalServed, + TxHash, + Value, +}; + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone, schemars::JsonSchema)] +pub enum StateProofKey { + Metadata(Metadata), + Accounts(EthAddress), + ClientKeys(ClientPublicKey), + Nodes(NodeIndex), + ConsensusKeyToIndex(ConsensusPublicKey), + PubKeyToIndex(NodePublicKey), + Latencies((NodeIndex, NodeIndex)), + Committees(Epoch), + Services(ServiceId), + Parameters(ProtocolParams), + ReputationMeasurements(NodeIndex), + ReputationScores(NodeIndex), + SubmittedReputationMeasurements(NodeIndex), + CurrentEpochServed(NodeIndex), + LastEpochServed(NodeIndex), + TotalServed(Epoch), + CommodityPrices(CommodityTypes), + ServiceRevenues(ServiceId), + ExecutedDigests(TxHash), + Uptime(NodeIndex), + UriToNode(Blake3Hash), + NodeToUri(NodeIndex), +} + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone, schemars::JsonSchema)] +#[allow(clippy::large_enum_variant)] +pub enum StateProofValue { + Metadata(Value), + Accounts(AccountInfo), + ClientKeys(EthAddress), + Nodes(NodeInfo), + ConsensusKeyToIndex(NodeIndex), + PubKeyToIndex(NodeIndex), + Latencies(Duration), + Committees(Committee), + Services(Service), + Parameters(u128), + ReputationMeasurements(Vec), + ReputationScores(u8), + SubmittedReputationMeasurements(u8), + CurrentEpochServed(NodeServed), + LastEpochServed(NodeServed), + TotalServed(TotalServed), + CommodityPrices(HpUfixed<6>), + ServiceRevenues(ServiceRevenue), + ExecutedDigests(()), + Uptime(u8), + UriToNode(BTreeSet), + NodeToUri(BTreeSet), +} + +impl StateProofKey { + /// Returns the table name for the given key. + pub fn table(&self) -> &str { + match self { + Self::Metadata(_) => "metadata", + Self::Accounts(_) => "account", + Self::ClientKeys(_) => "client_keys", + Self::Nodes(_) => "node", + Self::ConsensusKeyToIndex(_) => "consensus_key_to_index", + Self::PubKeyToIndex(_) => "pub_key_to_index", + Self::Latencies(_) => "latencies", + Self::Committees(_) => "committee", + Self::Services(_) => "service", + Self::Parameters(_) => "parameter", + Self::ReputationMeasurements(_) => "rep_measurements", + Self::ReputationScores(_) => "rep_scores", + Self::SubmittedReputationMeasurements(_) => "submitted_rep_measurements", + Self::CurrentEpochServed(_) => "current_epoch_served", + Self::LastEpochServed(_) => "last_epoch_served", + Self::TotalServed(_) => "total_served", + Self::CommodityPrices(_) => "commodity_prices", + Self::ServiceRevenues(_) => "service_revenue", + Self::ExecutedDigests(_) => "executed_digests", + Self::Uptime(_) => "uptime", + Self::UriToNode(_) => "uri_to_node", + Self::NodeToUri(_) => "node_to_uri", + } + } + + /// Returns the table name and serialized key value as a pair. + pub fn raw(&self) -> (String, Vec) { + let (table, key) = match self { + Self::Metadata(key) => (self.table(), S::serialize(key)), + Self::Accounts(key) => (self.table(), S::serialize(key)), + Self::ClientKeys(key) => (self.table(), S::serialize(key)), + Self::Nodes(key) => (self.table(), S::serialize(key)), + Self::ConsensusKeyToIndex(key) => (self.table(), S::serialize(key)), + Self::PubKeyToIndex(key) => (self.table(), S::serialize(key)), + Self::Latencies(key) => (self.table(), S::serialize(key)), + Self::Committees(key) => (self.table(), S::serialize(key)), + Self::Services(key) => (self.table(), S::serialize(key)), + Self::Parameters(key) => (self.table(), S::serialize(key)), + Self::ReputationMeasurements(key) => (self.table(), S::serialize(key)), + Self::ReputationScores(key) => (self.table(), S::serialize(key)), + Self::SubmittedReputationMeasurements(key) => (self.table(), S::serialize(key)), + Self::CurrentEpochServed(key) => (self.table(), S::serialize(key)), + Self::LastEpochServed(key) => (self.table(), S::serialize(key)), + Self::TotalServed(key) => (self.table(), S::serialize(key)), + Self::CommodityPrices(key) => (self.table(), S::serialize(key)), + Self::ServiceRevenues(key) => (self.table(), S::serialize(key)), + Self::ExecutedDigests(key) => (self.table(), S::serialize(key)), + Self::Uptime(key) => (self.table(), S::serialize(key)), + Self::UriToNode(key) => (self.table(), S::serialize(key)), + Self::NodeToUri(key) => (self.table(), S::serialize(key)), + }; + (table.to_string(), key) + } + + /// Returns the deserialized value for the given table/key. + pub fn value(&self, value: Vec) -> StateProofValue { + match self { + Self::Metadata(_) => StateProofValue::Metadata(S::deserialize(&value)), + Self::Accounts(_) => StateProofValue::Accounts(S::deserialize(&value)), + Self::ClientKeys(_) => StateProofValue::ClientKeys(S::deserialize(&value)), + Self::Nodes(_) => StateProofValue::Nodes(S::deserialize(&value)), + Self::ConsensusKeyToIndex(_) => { + StateProofValue::ConsensusKeyToIndex(S::deserialize(&value)) + }, + Self::PubKeyToIndex(_) => StateProofValue::PubKeyToIndex(S::deserialize(&value)), + Self::Latencies(_) => StateProofValue::Latencies(S::deserialize(&value)), + Self::Committees(_) => StateProofValue::Committees(S::deserialize(&value)), + Self::Services(_) => StateProofValue::Services(S::deserialize(&value)), + Self::Parameters(_) => StateProofValue::Parameters(S::deserialize(&value)), + Self::ReputationMeasurements(_) => { + StateProofValue::ReputationMeasurements(S::deserialize(&value)) + }, + Self::ReputationScores(_) => StateProofValue::ReputationScores(S::deserialize(&value)), + Self::SubmittedReputationMeasurements(_) => { + StateProofValue::SubmittedReputationMeasurements(S::deserialize(&value)) + }, + Self::CurrentEpochServed(_) => { + StateProofValue::CurrentEpochServed(S::deserialize(&value)) + }, + Self::LastEpochServed(_) => StateProofValue::LastEpochServed(S::deserialize(&value)), + Self::TotalServed(_) => StateProofValue::TotalServed(S::deserialize(&value)), + Self::CommodityPrices(_) => StateProofValue::CommodityPrices(S::deserialize(&value)), + Self::ServiceRevenues(_) => StateProofValue::ServiceRevenues(S::deserialize(&value)), + Self::ExecutedDigests(_) => { + S::deserialize::<()>(&value); + StateProofValue::ExecutedDigests(()) + }, + Self::Uptime(_) => StateProofValue::Uptime(S::deserialize(&value)), + Self::UriToNode(_) => StateProofValue::UriToNode(S::deserialize(&value)), + Self::NodeToUri(_) => StateProofValue::NodeToUri(S::deserialize(&value)), + } + } +}