diff --git a/Cargo.lock b/Cargo.lock index aff98fa..eefff46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -831,6 +831,55 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.30", + "itoa 1.0.11", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.71" @@ -1869,6 +1918,7 @@ dependencies = [ "ahash", "async-redis-session", "async-trait", + "axum", "base64 0.22.1", "bellman", "bls12_381", @@ -1879,7 +1929,7 @@ dependencies = [ "config", "crypto-hash", "ctrlc", - "dirs", + "dirs 5.0.1", "dotenvy", "ed25519", "ed25519-dalek", @@ -1908,6 +1958,9 @@ dependencies = [ "thiserror", "tokio", "toml 0.8.15", + "tower-http", + "utoipa", + "utoipa-swagger-ui", ] [[package]] @@ -1974,13 +2027,33 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "dirs-sys", + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", ] [[package]] @@ -3492,6 +3565,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" + [[package]] name = "httparse" version = "1.9.4" @@ -3721,6 +3800,7 @@ checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown 0.14.5", + "serde", ] [[package]] @@ -4240,6 +4320,12 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "maybe-async" version = "0.2.10" @@ -4272,6 +4358,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -5649,6 +5745,41 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" +[[package]] +name = "rust-embed" +version = "6.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "shellexpand", + "syn 2.0.71", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" +dependencies = [ + "sha2 0.10.8", + "walkdir", +] + [[package]] name = "rust-ini" version = "0.19.0" @@ -5854,6 +5985,12 @@ dependencies = [ "url", ] +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + [[package]] name = "ryu" version = "1.0.18" @@ -5994,6 +6131,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa 1.0.11", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.19" @@ -6104,6 +6251,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +[[package]] +name = "shellexpand" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +dependencies = [ + "dirs 4.0.0", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -6704,6 +6860,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "futures-core", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "http-range-header", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.2" @@ -6844,6 +7018,15 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -6929,6 +7112,47 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utoipa" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82b1bc5417102a73e8464c686eef947bdfb99fcdfc0a4f228e81afa9526470a" +dependencies = [ + "indexmap 2.2.6", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d96dcd6fc96f3df9b3280ef480770af1b7c5d14bc55192baa9b067976d920c" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 2.0.71", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84614caa239fb25b2bb373a52859ffd94605ceb256eeb1d63436325cf81e3653" +dependencies = [ + "axum", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "utoipa", + "zip", +] + [[package]] name = "uuid" version = "1.10.0" @@ -7371,6 +7595,18 @@ dependencies = [ "syn 2.0.71", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] + [[package]] name = "zstd" version = "0.13.2" diff --git a/Cargo.toml b/Cargo.toml index cc07b7b..4633e5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,10 @@ key_transparency = [] ci = [] [dependencies] +axum = "0.6" +tower-http = { version = "0.4", features = ["cors"] } +utoipa = { version = "3.3", features = ["axum_extras"] } +utoipa-swagger-ui = { version = "3.1", features = ["axum"] } crypto-hash = "0.3.4" async-trait = "0.1.68" serde = { version = "1.0.151", features = ["derive"] } diff --git a/src/da/mod.rs b/src/da/mod.rs index 25d98b9..e789a47 100644 --- a/src/da/mod.rs +++ b/src/da/mod.rs @@ -1,6 +1,6 @@ use crate::{ error::{DAResult, DeimosResult, GeneralError}, - utils::Signable, + utils::SignedContent, zk_snark::{Bls12Proof, VerifyingKey}, }; use async_trait::async_trait; @@ -22,7 +22,7 @@ pub struct EpochJson { pub signature: Option, } -impl Signable for EpochJson { +impl SignedContent for EpochJson { fn get_signature(&self) -> DeimosResult { match &self.signature { Some(signature) => Signature::from_str(signature) @@ -31,7 +31,7 @@ impl Signable for EpochJson { } } - fn get_content_to_sign(&self) -> DeimosResult { + fn get_plaintext(&self) -> DeimosResult { let mut copy = self.clone(); copy.signature = None; serde_json::to_string(©).map_err(|e| GeneralError::EncodingError(e.to_string()).into()) diff --git a/src/error.rs b/src/error.rs index ff529b6..cbeed00 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,4 @@ +use ed25519_dalek::SignatureError; use indexed_merkle_tree::error::MerkleTreeError; use thiserror::Error; @@ -35,8 +36,8 @@ pub enum GeneralError { MissingArgumentError(String), #[error("invalid public key")] InvalidPublicKey, - #[error("invalid signature")] - InvalidSignature, + #[error(transparent)] + InvalidSignature(#[from] SignatureError), #[error("starting webserver")] WebserverError, #[error("initializing service: {0}")] diff --git a/src/node_types/mod.rs b/src/node_types/mod.rs index 1a83355..fb16b1d 100644 --- a/src/node_types/mod.rs +++ b/src/node_types/mod.rs @@ -10,64 +10,3 @@ pub trait NodeType { async fn start(self: Arc) -> DeimosResult<()>; // async fn stop(&self) -> Result<(), String>; } - -#[cfg(test)] -mod tests { - use crate::{utils::verify_signature, webserver::UpdateEntryJson}; - use base64::{engine::general_purpose, Engine as _}; - - fn setup_signature(valid_signature: bool) -> UpdateEntryJson { - let signed_message = if valid_signature { - "NRtq1sgoxllsPvljXZd5f4DV7570PdA9zWHa4ych2jBCDU1uUYXZvW72BS9O+C68hptk/4Y34sTJj4x92gq9DHsiaWQiOiJDb3NSWE9vU0xHN2E4c0NHeDc4S2h0ZkxFdWl5Tlk3TDRrc0Z0NzhtcDJNPSIsIm9wZXJhdGlvbiI6IkFkZCIsInZhbHVlIjoiMjE3OWM0YmIzMjc0NDQ1NGE0OTlhYTMwZTI0NTJlMTZhODcwMGQ5ODQyYjI5ZThlODcyN2VjMzczNWMwYjdhNiJ9".to_string() - } else { - "QVmk3wgoxllsPvljXZd5f4DV7570PdA9zWHa4ych2jBCDU1uUYXZvW72BS9O+C68hptk/4Y34sTJj4x92gq9DHsiaWQiOiJDb3NSWE9vU0xHN2E4c0NHeDc4S2h0ZkxFdWl5Tlk3TDRrc0Z0NzhtcDJNPSIsIm9wZXJhdGlvbiI6IkFkZCIsInZhbHVlIjoiMjE3OWM0YmIzMjc0NDQ1NGE0OTlhYTMwZTI0NTJlMTZhODcwMGQ5ODQyYjI5ZThlODcyN2VjMzczNWMwYjdhNiJ9".to_string() - }; - let id_public_key = "CosRXOoSLG7a8sCGx78KhtfLEuiyNY7L4ksFt78mp2M=".to_string(); - - UpdateEntryJson { - signed_incoming_entry: signed_message, - public_key: id_public_key, - } - } - - #[test] - fn test_verify_valid_signature() { - let signature_with_key = setup_signature(true); - - let result = verify_signature( - &signature_with_key, - Some(signature_with_key.public_key.clone()), - ); - - assert!(result.is_ok()); - } - - #[test] - fn test_verify_invalid_signature() { - let signature_with_key = setup_signature(false); - - let result = verify_signature( - &signature_with_key, - Some(signature_with_key.public_key.clone()), - ); - assert!(result.is_err()); - } - - #[test] - fn test_verify_short_message() { - let signature_with_key = setup_signature(true); - - let short_message = general_purpose::STANDARD.encode("this is a short message"); - - let signature_with_key = UpdateEntryJson { - signed_incoming_entry: short_message, - ..signature_with_key - }; - - let result = verify_signature( - &signature_with_key, - Some(signature_with_key.public_key.clone()), - ); - assert!(result.is_err()); - } -} diff --git a/src/node_types/sequencer.rs b/src/node_types/sequencer.rs index dacc57e..d38f9fc 100644 --- a/src/node_types/sequencer.rs +++ b/src/node_types/sequencer.rs @@ -4,7 +4,12 @@ use crate::{ }; use async_trait::async_trait; use ed25519_dalek::{Signer, SigningKey}; -use indexed_merkle_tree::{node::Node, sha256_mod, tree::IndexedMerkleTree, Hash}; +use indexed_merkle_tree::{ + node::Node, + sha256_mod, + tree::{IndexedMerkleTree, Proof}, + Hash, +}; use std::{self, sync::Arc, time::Duration}; use tokio::{ sync::{ @@ -34,6 +39,9 @@ pub struct Sequencer { pub ws: WebServer, pub key: SigningKey, + pending_entries: Arc>>, + tree: Arc>, + epoch_buffer_tx: Arc>, epoch_buffer_rx: Arc>>, } @@ -66,7 +74,9 @@ impl NodeType for Sequencer { let main_loop = self.clone().main_loop(); let da_loop = self.clone().da_loop(); - let ws = self.clone().ws.start(self.clone()); + + let ws_self = self.clone(); + let ws = ws_self.ws.start(self.clone()); tokio::select! { _ = main_loop => Ok(()), @@ -107,6 +117,8 @@ impl Sequencer { epoch_duration, ws, key, + tree: Arc::new(Mutex::new(IndexedMerkleTree::new_with_size(1024).unwrap())), + pending_entries: Arc::new(Mutex::new(Vec::new())), epoch_buffer_tx: Arc::new(tx), epoch_buffer_rx: Arc::new(Mutex::new(rx)), }) @@ -185,40 +197,18 @@ impl Sequencer { .await } - /// Initializes the epoch state by setting up the input table and incrementing the epoch number. - /// Periodically calls the `set_epoch_commitment` function to update the commitment for the current epoch. - /// - /// # Behavior - /// 1. Initializes the input table by inserting an empty hash if it is empty. - /// 2. Updates the epoch number in the app state. - /// 3. Waits for a specified duration before starting the next epoch. - /// 4. Calls `set_epoch_commitment` to fetch and set the commitment for the current epoch. - /// 5. Repeats steps 2-4 periodically. + pub async fn get_commitment(&self) -> DeimosResult { + let tree = self.tree.lock().await; + tree.get_commitment().map_err(|e| e.into()) + } + + // finalize_epoch is responsible for finalizing the pending epoch and returning the epoch json to be posted on the DA layer. pub async fn finalize_epoch(&self) -> DeimosResult { let epoch = match self.db.get_epoch() { Ok(epoch) => epoch + 1, Err(_) => 0, }; - self.db.set_epoch(&epoch)?; - self.db.reset_epoch_operation_counter()?; - - // add the commitment for the operations ran since the last epoch - let current_commitment = self - .create_tree()? - .get_commitment() - .map_err(DeimosError::MerkleTree)?; - - self.db.add_commitment(&epoch, ¤t_commitment)?; - - let proofs = match epoch > 0 { - true => match self.db.get_proofs_in_epoch(&(epoch - 1)) { - Ok(proofs) => proofs, - Err(e) => return Err(DatabaseError::ReadError(e.to_string()).into()), - }, - false => vec![], - }; - let prev_commitment = if epoch > 0 { let prev_epoch = epoch - 1; match self.db.get_commitment(&prev_epoch) { @@ -233,12 +223,20 @@ impl Sequencer { } } } else { - let empty_commitment = self.create_tree()?; - empty_commitment - .get_commitment() - .map_err(DeimosError::MerkleTree)? + self.get_commitment().await? + }; + + let proofs = self.finalize_pending_entries().await?; + + let current_commitment = { + let tree = self.tree.lock().await; + tree.get_commitment().map_err(DeimosError::MerkleTree)? }; + self.db.set_epoch(&epoch)?; + // add the commitment for the operations ran since the last epoch + self.db.add_commitment(&epoch, ¤t_commitment)?; + let batch_circuit = BatchMerkleProofCircuit::new(&prev_commitment, ¤t_commitment, proofs)?; let (proof, verifying_key) = batch_circuit.create_and_verify_snark()?; @@ -270,124 +268,22 @@ impl Sequencer { } } - pub fn create_tree(&self) -> DeimosResult { - // TODO: better error handling (#11) - // Retrieve the keys from input order and sort them. - let ordered_derived_dict_keys: Vec = - self.db.get_derived_keys_in_order().unwrap_or_default(); - let mut sorted_keys = ordered_derived_dict_keys.clone(); - sorted_keys.sort(); - - // Initialize the leaf nodes with the value corresponding to the given key. Set the next node to the tail for now. - let nodes_result: Result, DatabaseError> = sorted_keys - .iter() - .map(|key| { - let value: String = self - .db - .get_derived_value(&key.to_string()) - .map_err(|e| DatabaseError::ReadError(e.to_string()))?; - let hash_key = Hash::from_hex(key).unwrap(); - let hash_value = Hash::from_hex(&value).unwrap(); - Ok(Node::new_leaf(true, true, hash_key, hash_value, Node::TAIL)) - }) - .collect(); - - let mut nodes: Vec = nodes_result?; - - // calculate the next power of two, tree size is at least 8 for now - let mut next_power_of_two: usize = 8; - while next_power_of_two < ordered_derived_dict_keys.len() + 1 { - next_power_of_two *= 2; + async fn finalize_pending_entries(&self) -> DeimosResult> { + let mut pending_entries = self.pending_entries.lock().await; + let mut proofs = Vec::new(); + for entry in pending_entries.iter() { + let proof = self.update_entry(entry).await?; + proofs.push(proof); } - - // Calculate the node hashes and sort the keys (right now they are sorted, so the next node is always the one bigger than the current one) - for i in 0..nodes.len() - 1 { - let is_next_node_active = nodes[i + 1].is_active(); - if is_next_node_active { - let next_label = match &nodes[i + 1] { - Node::Leaf(next_leaf) => next_leaf.label.clone(), - _ => unreachable!(), - }; - - if let Node::Leaf(leaf) = &mut nodes[i] { - leaf.next = next_label; - } - - nodes[i].generate_hash(); - } - } - - // resort the nodes based on the input order - nodes.sort_by_cached_key(|node| { - let label = match node { - Node::Inner(_) => None, - Node::Leaf(leaf) => { - let label = leaf.label.clone(); - Some(label) - } - }; - - ordered_derived_dict_keys - .iter() - .enumerate() // use index - .find(|(_, k)| { - let k = Hash::from_hex(k).unwrap(); - label.clone().is_some_and(|l| k == l) - }) - .map(|(k, _)| k) - }); - - // Add empty nodes to ensure the total number of nodes is a power of two. - while nodes.len() < next_power_of_two { - nodes.push(Node::new_leaf( - false, - true, - Node::HEAD, - Node::HEAD, - Node::TAIL, - )); - } - - // create tree, setting left / right child property for each node - IndexedMerkleTree::new(nodes).map_err(DeimosError::MerkleTree) + pending_entries.clear(); + Ok(proofs) } - /// Updates an entry in the database based on the given operation, incoming entry, and the signature from the user. - /// - /// # Arguments - /// - /// * `signed_entry` - A `UpdateEntryJson` object. - pub fn update_entry(&self, signed_entry: &UpdateEntryJson) -> DeimosResult<()> { - let signed_content = - match verify_signature(signed_entry, Some(signed_entry.public_key.clone())) { - Ok(content) => content, - Err(_) => { - // TODO(@distractedm1nd): Add to error instead of logging - error!( - "updating entry: invalid signature with pubkey {} on msg {}", - signed_entry.public_key, signed_entry.signed_incoming_entry - ); - return Err(GeneralError::InvalidSignature.into()); - } - }; - - let message_obj: IncomingEntry = match serde_json::from_str(&signed_content) { - Ok(obj) => obj, - Err(e) => { - return Err(GeneralError::ParsingError(format!("signed content: {}", e)).into()); - } - }; - - let id = message_obj.id.clone(); - - // check with given key if the signature is valid - let incoming_entry = IncomingEntry { - id: id.clone(), - operation: message_obj.operation, - value: message_obj.value, - }; + /// Updates the state from on a pending incoming entry. + async fn update_entry(&self, incoming_entry: &IncomingEntry) -> DeimosResult { + let id = incoming_entry.id.clone(); // add a new key to an existing id ( type for the value retrieved from the database explicitly set to string) - match self.db.get_hashchain(&id) { + let hashchain: DeimosResult> = match self.db.get_hashchain(&id) { Ok(value) => { // hashchain already exists let mut current_chain = value.clone(); @@ -416,7 +312,7 @@ impl Sequencer { }; current_chain.push(new_chain_entry.clone()); - match self.db.update_hashchain(&incoming_entry, ¤t_chain) { + match self.db.update_hashchain(incoming_entry, ¤t_chain) { Ok(_) => (), Err(_) => { return Err(DatabaseError::WriteError(format!( @@ -428,7 +324,7 @@ impl Sequencer { } match self .db - .set_derived_entry(&incoming_entry, &new_chain_entry, false) + .set_derived_entry(incoming_entry, &new_chain_entry, false) { Ok(_) => (), Err(_) => { @@ -440,10 +336,10 @@ impl Sequencer { } } - Ok(()) + Ok(value) } - Err(_) => { - debug!("Hashchain does not exist, creating new one..."); + Err(e) => { + debug!("creating new hashchain for user id {}", id.clone()); let new_chain = vec![ChainEntry { hash: { let mut data = Vec::new(); @@ -466,7 +362,7 @@ impl Sequencer { .into()); } }; - match self.db.update_hashchain(&incoming_entry, &new_chain) { + match self.db.update_hashchain(incoming_entry, &new_chain) { Ok(_) => (), Err(_) => { return Err(DatabaseError::WriteError(format!( @@ -476,8 +372,9 @@ impl Sequencer { .into()); } } - match self.db.set_derived_entry(&incoming_entry, last_entry, true) { - Ok(_) => Ok(()), + match self.db.set_derived_entry(incoming_entry, last_entry, true) { + // we return the error so that the node is updated rather than inserted + Ok(_) => Err(e), Err(_) => Err(DatabaseError::WriteError(format!( "derived entry for incoming entry {:?}", incoming_entry @@ -485,6 +382,210 @@ impl Sequencer { .into()), } } + }; + + let mut tree = self.tree.lock().await; + let hashed_id = sha256_mod(id.as_bytes()); + + // todo: not all error cases make it okay to continue here, so we should filter by a Hashchain key not found error + if hashchain.is_ok() { + let node = match tree.find_leaf_by_label(&hashed_id) { + Some(node) => node, + None => { + // TODO: before merging, change error type + return Err(GeneralError::DecodingError(format!( + "node with label {} not found in the tree", + hashed_id + )) + .into()); + } + }; + let new_index = match tree.find_node_index(&node) { + Some(index) => index, + None => { + return Err(GeneralError::DecodingError(format!( + "node with label {} not found in the tree, but has a hashchain entry", + hashed_id + )) + .into()); + } + }; + // TODO: Possible optimization: cache the last update proof for each id for serving the proofs + tree.update_node(new_index, node) + .map(Proof::Update) + .map_err(|e| e.into()) + } else { + let mut node = Node::new_leaf( + true, + true, + hashed_id, + sha256_mod(incoming_entry.value.as_bytes()), + sha256_mod("PLACEHOLDER".as_bytes()), + ); + tree.insert_node(&mut node) + .map(Proof::Insert) + .map_err(|e| e.into()) } } + + /// Adds an update to be applied in the next epoch. + /// + /// # Arguments + /// + /// * `signed_entry` - A `UpdateEntryJson` object. + pub async fn validate_and_queue_update( + self: Arc, + signed_entry: &UpdateEntryJson, + ) -> DeimosResult<()> { + let signed_content = match verify_signature(signed_entry, None) { + Ok(content) => content, + Err(e) => { + // TODO(@distractedm1nd): Add to error instead of logging + error!( + "updating entry: invalid signature with pubkey {} on msg {}", + signed_entry.public_key, signed_entry.signed_incoming_entry + ); + return Err(e); + } + }; + + let incoming: IncomingEntry = match serde_json::from_str(&signed_content) { + Ok(obj) => obj, + Err(e) => { + return Err(GeneralError::ParsingError(format!("signed content: {}", e)).into()); + } + }; + + let mut pending = self.pending_entries.lock().await; + pending.push(incoming); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cfg::{Config, RedisConfig}; + use crate::da::mock::LocalDataAvailabilityLayer; + use crate::storage::RedisConnection; + use base64::{engine::general_purpose::STANDARD as engine, Engine as _}; + use keystore_rs::create_signing_key; + + // set up redis connection and flush database before each test + fn setup_db() -> RedisConnection { + let redis_connection = RedisConnection::new(&RedisConfig::default()).unwrap(); + redis_connection.flush_database().unwrap(); + redis_connection + } + + // flush database after each test + fn teardown_db(redis_connections: &RedisConnection) { + redis_connections.flush_database().unwrap(); + } + + fn create_update_entry(id: String, value: String) -> UpdateEntryJson { + let key = create_signing_key(); + let incoming = IncomingEntry { + id, + operation: Operation::Add, + value, + }; + let content = serde_json::to_string(&incoming).unwrap(); + let sig = key.sign(content.clone().as_bytes()); + + UpdateEntryJson { + incoming_entry: incoming, + signed_incoming_entry: sig.to_string(), + public_key: engine.encode(key.verifying_key().to_bytes()), + } + } + + #[tokio::test] + async fn test_validate_and_queue_update() { + let da_layer = Arc::new(LocalDataAvailabilityLayer::new()); + let db = Arc::new(setup_db()); + let sequencer = Arc::new( + Sequencer::new( + db.clone(), + da_layer, + Config::default(), + create_signing_key(), + ) + .unwrap(), + ); + + let update_entry = + create_update_entry("test@deltadevs.xyz".to_string(), "test".to_string()); + + sequencer + .validate_and_queue_update(&update_entry) + .await + .unwrap(); + teardown_db(&db); + } + + #[tokio::test] + async fn test_queued_update_gets_finalized() { + let da_layer = Arc::new(LocalDataAvailabilityLayer::new()); + let db = Arc::new(setup_db()); + let sequencer = Arc::new( + Sequencer::new( + db.clone(), + da_layer, + Config::default(), + create_signing_key(), + ) + .unwrap(), + ); + + let id = "test@deltadevs.xyz".to_string(); + let update_entry = create_update_entry(id.clone(), "test".to_string()); + + sequencer + .clone() + .validate_and_queue_update(&update_entry) + .await + .unwrap(); + + // hashchain doesn't exist yet, because operation is only queued + let hashchain = sequencer.db.get_hashchain(id.as_str()); + assert!(hashchain.is_err()); + + let prev_commitment = sequencer.get_commitment().await.unwrap(); + sequencer.finalize_epoch().await.unwrap(); + let new_commitment = sequencer.get_commitment().await.unwrap(); + assert_ne!(prev_commitment, new_commitment); + + let hashchain = sequencer.db.get_hashchain(id.as_str()); + assert_eq!( + hashchain.unwrap().first().unwrap().value, + sha256_mod("test".as_bytes()) + ); + + teardown_db(&db); + } + + #[tokio::test] + async fn test_validate_invalid_update_fails() { + let da_layer = Arc::new(LocalDataAvailabilityLayer::new()); + let db = Arc::new(setup_db()); + let sequencer = Arc::new( + Sequencer::new( + db.clone(), + da_layer, + Config::default(), + create_signing_key(), + ) + .unwrap(), + ); + + let mut update_entry = + create_update_entry("test@deltadevs.xyz".to_string(), "test".to_string()); + let second_signer = create_update_entry("abcd".to_string(), "test".to_string()).public_key; + update_entry.public_key = second_signer; + + let res = sequencer.validate_and_queue_update(&update_entry).await; + assert!(res.is_err()); + teardown_db(&db); + } } diff --git a/src/storage.rs b/src/storage.rs index 9414e2b..a66dfa9 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -52,7 +52,7 @@ pub struct DerivedEntry { pub value: String, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Clone, Serialize, Deserialize, Debug)] pub struct IncomingEntry { pub id: String, pub operation: Operation, @@ -81,9 +81,7 @@ pub trait Database: Send + Sync { fn get_proof(&self, id: &str) -> DeimosResult; fn get_proofs_in_epoch(&self, epoch: &u64) -> DeimosResult>; fn get_epoch(&self) -> DeimosResult; - fn get_epoch_operation(&self) -> DeimosResult; fn set_epoch(&self, epoch: &u64) -> DeimosResult<()>; - fn reset_epoch_operation_counter(&self) -> DeimosResult<()>; fn update_hashchain( &self, incoming_entry: &IncomingEntry, @@ -96,7 +94,6 @@ pub trait Database: Send + Sync { new: bool, ) -> DeimosResult<()>; fn get_epochs(&self) -> DeimosResult>; - fn increment_epoch_operation(&self) -> DeimosResult; fn add_merkle_proof( &self, epoch: &u64, @@ -175,7 +172,10 @@ impl Database for RedisConnection { fn get_hashchain(&self, key: &str) -> DeimosResult> { let mut con = self.lock_connection()?; let value: String = con.get(format!("main:{}", key)).map_err(|_| { - DeimosError::Database(DatabaseError::NotFoundError(format!("key: {}", key))) + DeimosError::Database(DatabaseError::NotFoundError(format!( + "hashchain key {}", + key + ))) })?; serde_json::from_str(&value).map_err(|e| { @@ -185,9 +185,13 @@ impl Database for RedisConnection { fn get_derived_value(&self, key: &str) -> DeimosResult { let mut con = self.lock_connection()?; - con.get(format!("derived:{}", key)).map_err(|_| { - DeimosError::Database(DatabaseError::NotFoundError(format!("key: {}", key))) - }) + con.get::(format!("derived:{}", key)) + .map_err(|e| { + DeimosError::Database(DatabaseError::NotFoundError(format!( + "derived key {} with err {}: ", + key, e + ))) + }) } // TODO: noticed a strange behavior with the get_derived_keys() function, it returns the values in seemingly random order. Need to investigate more @@ -255,13 +259,6 @@ impl Database for RedisConnection { }) } - fn get_epoch_operation(&self) -> DeimosResult { - let mut con = self.lock_connection()?; - con.get("app_state:epoch_operation").map_err(|_| { - DeimosError::Database(DatabaseError::NotFoundError("epoch operation".to_string())) - }) - } - fn set_epoch(&self, epoch: &u64) -> DeimosResult<()> { let mut con = self.lock_connection()?; con.set::<&str, &u64, ()>("app_state:epoch", epoch) @@ -270,14 +267,6 @@ impl Database for RedisConnection { }) } - fn reset_epoch_operation_counter(&self) -> DeimosResult<()> { - let mut con = self.lock_connection()?; - con.set::<&str, &u64, ()>("app_state:epoch_operation", &0) - .map_err(|_| { - DeimosError::Database(DatabaseError::WriteError("epoch_operation->0".to_string())) - }) - } - fn update_hashchain( &self, incoming_entry: &IncomingEntry, @@ -306,7 +295,8 @@ impl Database for RedisConnection { ) -> DeimosResult<()> { let mut con = self.lock_connection()?; let hashed_key = sha256_mod(incoming_entry.id.as_bytes()); - con.set::<&str, &[u8], String>(&format!("derived:{}", hashed_key), value.hash.as_ref()) + let stored_value = hex::encode(value.hash.as_ref()); + con.set::<&str, String, String>(&format!("derived:{}", hashed_key), stored_value) .map_err(|_| { DeimosError::Database(DatabaseError::WriteError(format!( "derived dict update for key: {}", @@ -346,14 +336,6 @@ impl Database for RedisConnection { .collect() } - fn increment_epoch_operation(&self) -> DeimosResult { - let mut con = self.lock_connection()?; - con.incr::<&'static str, u64, u64>("app_state:epoch_operation", 1) - .map_err(|_| { - DeimosError::Database(DatabaseError::WriteError("incremented epoch".to_string())) - }) - } - fn add_merkle_proof( &self, epoch: &u64, diff --git a/src/utils.rs b/src/utils.rs index 17ba47b..a722776 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -20,15 +20,16 @@ use rand::rngs::OsRng; /// /// # Returns /// -/// `true` if the value was not revoked, otherwise `false`. +/// `true` if the value was revoked, otherwise `false`. /// TODO(@distractedm1nd): is_revoked > is_not_revoked, for readability -pub fn is_not_revoked(entries: &[ChainEntry], value: Hash) -> bool { +#[allow(dead_code)] +pub fn is_revoked(entries: &[ChainEntry], value: Hash) -> bool { for entry in entries { if entry.value == value && matches!(entry.operation, Operation::Revoke) { - return false; + return true; } } - true + false } pub fn parse_json_to_proof(json_str: &str) -> Result> { @@ -41,7 +42,7 @@ pub fn decode_public_key(pub_key_str: &String) -> DeimosResult DeimosResult; - fn get_content_to_sign(&self) -> DeimosResult; + fn get_plaintext(&self) -> DeimosResult; fn get_public_key(&self) -> DeimosResult; } -pub fn decode_signed_message(signed_message: &String) -> DeimosResult> { - let signed_message_bytes = engine.decode(signed_message).map_err(|e| { - DeimosError::General(GeneralError::DecodingError(format!( - "signed message: {}", - e - ))) - })?; - - // check if the signed message is (at least) 64 bytes long - if signed_message_bytes.len() < 64 { - Err(GeneralError::ParsingError(format!( - "signed message is too short: {} < 64", - signed_message_bytes.len(), - )) - .into()) - } else { - Ok(signed_message_bytes) - } -} - // verifies the signature of a given signable item and returns the content of the item if the signature is valid -pub fn verify_signature( +pub fn verify_signature( item: &T, optional_public_key: Option, ) -> DeimosResult { @@ -148,13 +129,12 @@ pub fn verify_signature( let public_key = decode_public_key(&public_key_str) .map_err(|_| DeimosError::General(GeneralError::InvalidPublicKey))?; - let content = item.get_content_to_sign()?; + let content = item.get_plaintext()?; let signature = item.get_signature()?; - if public_key.verify(content.as_bytes(), &signature).is_ok() { - Ok(content) - } else { - Err(GeneralError::InvalidSignature.into()) + match public_key.verify(content.as_bytes(), &signature) { + Ok(_) => Ok(content), + Err(e) => Err(GeneralError::InvalidSignature(e).into()), } } diff --git a/src/webserver.rs b/src/webserver.rs index 8d2d805..ae02c22 100644 --- a/src/webserver.rs +++ b/src/webserver.rs @@ -1,28 +1,32 @@ use crate::{ cfg::WebServerConfig, - error::{DeimosError, DeimosResult, GeneralError}, + error::{DeimosResult, GeneralError}, node_types::sequencer::Sequencer, storage::{ChainEntry, IncomingEntry}, - utils::{decode_signed_message, is_not_revoked, Signable}, + utils::SignedContent, }; -use actix_cors::Cors; -use actix_web::{ - dev::Server, - get, post, - web::{self, Data}, - App as ActixApp, HttpResponse, HttpServer, Responder, +use axum::{ + extract::State, + http::StatusCode, + response::IntoResponse, + routing::{get, post}, + Json, Router, }; use ed25519::Signature; -use indexed_merkle_tree::{sha256_mod, tree::Proof}; +use indexed_merkle_tree::tree::{Proof, UpdateProof}; +use indexed_merkle_tree::Hash as TreeHash; use serde::{Deserialize, Serialize}; -use serde_json::{self, json, Value}; use std::sync::Arc; +use std::{self, str::FromStr}; +use tower_http::cors::CorsLayer; +use utoipa::{OpenApi, ToSchema}; +use utoipa_swagger_ui::SwaggerUi; pub struct WebServer { pub cfg: WebServerConfig, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, ToSchema)] pub struct EpochData { epoch_number: u64, previous_commitment: String, @@ -30,38 +34,54 @@ pub struct EpochData { proofs: Vec, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, ToSchema)] pub struct UpdateEntryJson { + pub incoming_entry: IncomingEntry, pub signed_incoming_entry: String, pub public_key: String, } -impl Signable for UpdateEntryJson { - fn get_signature(&self) -> DeimosResult { - let signed_message_bytes = decode_signed_message(&self.signed_incoming_entry)?; +#[derive(Serialize, Deserialize, ToSchema)] +pub struct UpdateProofResponse(UpdateProof); + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct Hash(TreeHash); - // extract the first 64 bytes from the signed message which are the signature - let signature_bytes: &[u8; 64] = match signed_message_bytes.get(..64) { - Some(array_section) => match array_section.try_into() { - Ok(array) => array, - Err(e) => Err(DeimosError::General(GeneralError::DecodingError(format!( - "signed message to array: {}", - e - ))))?, - }, - None => Err(DeimosError::General(GeneralError::DecodingError(format!( - "extracting signature from signed message: {}", - &self.signed_incoming_entry - ))))?, - }; +#[derive(Serialize, Deserialize, ToSchema)] +pub struct UserKeyRequest { + pub id: String, +} - Ok(Signature::from_bytes(signature_bytes)) +// TODO: Retrieve Merkle proof of current epoch +#[derive(Serialize, Deserialize, ToSchema)] +pub struct UserKeyResponse { + pub hashchain: Vec, + // pub proof: MerkleProof +} + +#[derive(OpenApi)] +#[openapi( + paths(update_entry, get_hashchain, get_commitment), + components(schemas( + UpdateEntryJson, + EpochData, + UpdateProofResponse, + Hash, + UserKeyRequest, + UserKeyResponse + )) +)] +struct ApiDoc; + +impl SignedContent for UpdateEntryJson { + fn get_signature(&self) -> DeimosResult { + Signature::from_str(self.signed_incoming_entry.as_str()) + .map_err(|e| GeneralError::ParsingError(format!("signature: {}", e)).into()) } - fn get_content_to_sign(&self) -> DeimosResult { - let signed_message_bytes = decode_signed_message(&self.signed_incoming_entry)?; - let message_bytes = &signed_message_bytes[64..]; - Ok(String::from_utf8_lossy(message_bytes).to_string()) + fn get_plaintext(&self) -> DeimosResult { + serde_json::to_string(&self.incoming_entry) + .map_err(|e| GeneralError::DecodingError(e.to_string()).into()) } fn get_public_key(&self) -> DeimosResult { @@ -71,244 +91,97 @@ impl Signable for UpdateEntryJson { impl WebServer { pub fn new(cfg: WebServerConfig) -> Self { - WebServer { cfg } + Self { cfg } } - pub fn start(&self, session: Arc) -> Server { - /* let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); - builder.set_private_key_file(env.key_path, SslFiletype::PEM).unwrap(); - builder.set_certificate_chain_file(env.cert_path).unwrap(); */ + pub async fn start(&self, session: Arc) { info!("starting webserver on {}:{}", self.cfg.host, self.cfg.port); - let ctx = Data::new(session.clone()); - let (ip, port) = (self.cfg.host.clone(), self.cfg.port); - - HttpServer::new(move || { - let cors = Cors::default() - .allow_any_origin() - .allow_any_method() - .allow_any_header(); - ActixApp::new() - .app_data(ctx.clone()) - .wrap(cors) - .service(get_commitment) - .service(update_entry) - .service(get_valid_keys) - }) - /* .bind_openssl((self.ip, self.port), builder)? */ - .bind((ip, port)) - .expect("Could not bind to port") - .run() + let app = Router::new() + .route("/update-entry", post(update_entry)) + .route("/get-hashchain", post(get_hashchain)) + .route("/get-current-commitment", get(get_commitment)) + .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi())) + .layer(CorsLayer::permissive()) + .with_state(session); + + let addr = format!("{}:{}", self.cfg.host, self.cfg.port); + axum::Server::bind(&addr.parse().unwrap()) + .serve(app.into_make_service()) + .await + .unwrap(); } } -/// Updates or inserts an entry in the dictionary and generates a Merkle proof. +/// Updates or inserts an entry in the transparency dictionary, pending inclusion in the next epoch. /// -/// # Arguments -/// -/// * `req_body` - A JSON string containing the information needed to update or insert an entry in the dictionary. -/// The JSON string should have the following fields: -/// - `signed_message`: An `UpdateEntryJson` object containing the id, operation, and value, signed by the public key. -/// -/// # Returns -/// -/// * `HttpResponse::Ok` with a success message if the update or insertion was successful. -/// * `HttpResponse::BadRequest` with an error message if the update or insertion fails. -/// -#[post("/update-entry")] +#[utoipa::path( + post, + path = "/update-entry", + request_body = UpdateEntryJson, + responses( + (status = 200, description = "Entry update queued for insertion into next epoch"), + (status = 400, description = "Bad request"), + (status = 500, description = "Internal server error") + ) +)] async fn update_entry( - session: web::Data>, - signature_with_key: web::Json, -) -> impl Responder { - let signature_with_key: UpdateEntryJson = - match serde_json::from_value(signature_with_key.into_inner()) { - Ok(entry_json) => entry_json, - Err(_) => { - return HttpResponse::BadRequest().json("Could not parse JSON data. Wrong format.") - } - }; - - let incoming_entry_json = match signature_with_key.get_content_to_sign() { - Ok(entry) => entry, - Err(e) => { - return HttpResponse::BadRequest().json(format!( - "Error retrieving content from UpdateEntryJson: {}", - e - )) - } - }; - - let incoming_entry: IncomingEntry = match serde_json::from_str(&incoming_entry_json) { - Ok(entry) => entry, - Err(e) => { - return HttpResponse::BadRequest().json(format!("Error decoding signed content: {}", e)) - } - }; - - let epoch = match session.db.get_epoch() { - Ok(e) => e, - Err(e) => { - return HttpResponse::InternalServerError().json(format!("Error getting epoch: {}", e)) - } - }; - - let epoch_operation = match session.db.get_epoch_operation() { - Ok(eo) => eo, - Err(e) => { - return HttpResponse::InternalServerError() - .json(format!("Error getting epoch operation: {}", e)) - } - }; - - let tree = match session.create_tree() { - Ok(t) => t, - Err(e) => { - return HttpResponse::InternalServerError().json(format!("Error creating tree: {}", e)) - } - }; - - let result: DeimosResult> = session.db.get_hashchain(&incoming_entry.id); - let update_proof = result.is_ok(); - - match session.update_entry(&signature_with_key) { - Ok(_) => { - let new_tree = match session.create_tree() { - Ok(t) => t, - Err(e) => { - return HttpResponse::InternalServerError() - .json(format!("Error creating new tree: {}", e)) - } - }; - let hashed_id = sha256_mod(incoming_entry.id.as_bytes()); - let mut node = match new_tree.find_leaf_by_label(&hashed_id) { - Some(n) => n, - None => return HttpResponse::InternalServerError().json("Error finding leaf"), - }; - - let proofs = if update_proof { - let new_index = match tree.clone().find_node_index(&node) { - Some(i) => i, - None => { - return HttpResponse::InternalServerError() - .json("Error finding node index: {}") - } - }; - let update_proof = match tree.clone().update_node(new_index, node) { - Ok(p) => p, - Err(e) => { - return HttpResponse::InternalServerError() - .json(format!("Error updating node: {}", e)) - } - }; - match serde_json::to_string(&update_proof) { - Ok(pre_processed_string) => format!(r#"{{"Update":{}}}"#, pre_processed_string), - Err(e) => { - return HttpResponse::InternalServerError() - .json(format!("Error serializing update proof: {}", e)) - } - } - } else { - let insert_proof = match tree.clone().insert_node(&mut node) { - Ok(p) => p, - Err(e) => { - return HttpResponse::InternalServerError() - .json(format!("Error inserting node: {}", e)) - } - }; - match serde_json::to_string(&insert_proof) { - Ok(pre_processed_string) => format!(r#"{{"Insert":{}}}"#, pre_processed_string), - Err(e) => { - return HttpResponse::InternalServerError() - .json(format!("Error serializing insert proof: {}", e)) - } - } - }; - - let commitment = match tree.get_commitment() { - Ok(c) => c, - Err(e) => { - return HttpResponse::InternalServerError() - .json(format!("Error getting commitment: {}", e)) - } - }; - - if let Err(err) = - session - .db - .add_merkle_proof(&epoch, &epoch_operation, &commitment, &proofs) - { - return HttpResponse::InternalServerError() - .json(format!("Error adding merkle proof: {}", err)); - } - - if let Err(err) = session.db.increment_epoch_operation() { - return HttpResponse::InternalServerError() - .json(format!("Error incrementing epoch operation: {}", err)); - } - - HttpResponse::Ok().body("Updated entry successfully") - } - Err(e) => HttpResponse::BadRequest().body(format!("Could not update entry: {}", e)), + State(session): State>, + Json(signature_with_key): Json, +) -> impl IntoResponse { + match session.validate_and_queue_update(&signature_with_key).await { + Ok(_) => ( + StatusCode::OK, + "Entry update queued for insertion into next epoch", + ) + .into_response(), + Err(e) => ( + StatusCode::BAD_REQUEST, + format!("Could not update entry: {}", e), + ) + .into_response(), } } -/// The /get-valid-keys endpoint calculates the non-revoked values associated with an ID. -/// -/// This endpoint takes a JSON request body containing an ID, for example: -/// { -/// "id": "bob@dom.org" -/// } -/// -/// The function retrieves the hashchain associated with the provided ID from the database. It then iterates through the hashchain to find all -/// the non-revoked keys. The resulting list of non-revoked keys is returned as a JSON object like the following: -/// { -/// "values": [public_key1, public_key2, ...] -/// } +/// The /get-hashchain endpoint returns all added keys for a given user id. /// -/// If the ID is not found in the database, the endpoint will return a BadRequest response with the message "Could not calculate values". +/// If the ID is not found in the database, the endpoint will return a 400 response with the message "Could not calculate values". /// -#[post("/get-valid-keys")] // all active values for a given id -async fn get_valid_keys(con: web::Data>, req_body: String) -> impl Responder { - let incoming_id: String = match serde_json::from_str(&req_body) { - Ok(id) => id, - Err(e) => return HttpResponse::BadRequest().body(format!("Invalid JSON: {}", e)), - }; - - match con.db.get_hashchain(&incoming_id) { - Ok(value) => { - let chain_copy = value.clone(); - let mut values = vec![]; - - // add all non-revoked keys to values vector - for entry in value { - if is_not_revoked(&chain_copy, entry.value.clone()) { - values.push(entry.value); - } - } - - match serde_json::to_string(&json!({ "values": values })) { - Ok(json_response) => HttpResponse::Ok().body(json_response), - Err(e) => HttpResponse::InternalServerError() - .body(format!("Failed to serialize response: {}", e)), - } - } - Err(err) => HttpResponse::BadRequest().body(format!("Couldn't calculate values: {}", err)), +#[utoipa::path( + post, + path = "/get-hashchain", + request_body = UserKeyRequest, + responses( + (status = 200, description = "Successfully retrieved valid keys", body = UpdateKeyResponse), + (status = 400, description = "Bad request") + ) +)] +async fn get_hashchain( + State(session): State>, + Json(request): Json, +) -> impl IntoResponse { + match session.db.get_hashchain(&request.id) { + Ok(hashchain) => (StatusCode::OK, Json(UserKeyResponse { hashchain })).into_response(), + Err(err) => ( + StatusCode::BAD_REQUEST, + format!("Couldn't get hashchain: {}", err), + ) + .into_response(), } } /// Returns the commitment (tree root) of the IndexedMerkleTree initialized from the database. /// -#[get("/get-current-commitment")] -async fn get_commitment(con: web::Data>) -> impl Responder { - match con.create_tree() { - Ok(tree) => match tree.get_commitment() { - Ok(commitment) => match serde_json::to_string(&commitment) { - Ok(serialized) => HttpResponse::Ok().body(serialized), - Err(_) => { - HttpResponse::InternalServerError().body("Failed to serialize commitment") - } - }, - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), - }, - Err(e) => HttpResponse::InternalServerError().body(e.to_string()), +#[utoipa::path( + get, + path = "/get-current-commitment", + responses( + (status = 200, description = "Successfully retrieved current commitment", body = Hash), + (status = 500, description = "Internal server error") + ) +)] +async fn get_commitment(State(session): State>) -> impl IntoResponse { + match session.get_commitment().await { + Ok(commitment) => (StatusCode::OK, Json(commitment)).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } }