From 8b6255f2eae55788d02f0d76c750db0adbf37ba5 Mon Sep 17 00:00:00 2001 From: "S. Santos" Date: Thu, 13 Jun 2024 16:28:53 -0300 Subject: [PATCH] Add multiple enclave support --- server/Settings.toml | 25 ++++++++++- server/migrations/0001_key_data_table.sql | 1 + server/src/database/deposit.rs | 5 ++- server/src/database/mod.rs | 1 + server/src/database/utils.rs | 24 +++++++++++ server/src/endpoints/deposit.rs | 51 +++++++++++++++++++---- server/src/endpoints/mod.rs | 10 ++--- server/src/endpoints/sign.rs | 44 +++++++++++++++++-- server/src/endpoints/transfer_receiver.rs | 39 +++++++++++++++-- server/src/endpoints/transfer_sender.rs | 4 +- server/src/endpoints/utils.rs | 15 +++---- server/src/endpoints/withdraw.rs | 19 ++++++++- server/src/server.rs | 4 +- server/src/server_config.rs | 40 +++++++++++++++--- 14 files changed, 239 insertions(+), 43 deletions(-) create mode 100644 server/src/database/utils.rs diff --git a/server/Settings.toml b/server/Settings.toml index 29142e42..6de0e77a 100644 --- a/server/Settings.toml +++ b/server/Settings.toml @@ -1,6 +1,27 @@ -lockbox = "http://0.0.0.0:18080" network = "testnet" lockheight_init = 1000 lh_decrement = 10 connection_string = "postgresql://postgres:postgres@localhost/mercury" -batch_timeout = 120 # seconds \ No newline at end of file +batch_timeout = 120 # seconds + +[[enclaves]] +url = "http://0.0.0.0:18080" +allow_deposit = true + +[[enclaves]] +url = "http://0.0.0.0:18080" +allow_deposit = false + +[[enclaves]] +url = "http://0.0.0.0:18080" +allow_deposit = true + +[[enclaves]] +url = "http://0.0.0.0:18080" +allow_deposit = true + +[[enclaves]] +url = "http://0.0.0.0:18080" +allow_deposit = true + +# env var: ENCLAVES='[{"url": "http://0.0.0.0:18080", "allow_deposit": true}, {"url": "http://0.0.0.0:18080", "allow_deposit": false}]' diff --git a/server/migrations/0001_key_data_table.sql b/server/migrations/0001_key_data_table.sql index 276fd15e..7690cca6 100644 --- a/server/migrations/0001_key_data_table.sql +++ b/server/migrations/0001_key_data_table.sql @@ -4,6 +4,7 @@ CREATE TABLE public.statechain_data ( auth_xonly_public_key bytea NULL, server_public_key bytea NULL UNIQUE, statechain_id varchar NULL UNIQUE, + enclave_index integer NOT NULL, CONSTRAINT statechain_data_pkey PRIMARY KEY (id), CONSTRAINT statechain_data_server_public_key_ukey UNIQUE (server_public_key) ); diff --git a/server/src/database/deposit.rs b/server/src/database/deposit.rs index 151ea00c..52056f92 100644 --- a/server/src/database/deposit.rs +++ b/server/src/database/deposit.rs @@ -63,15 +63,16 @@ pub async fn check_existing_key(pool: &sqlx::PgPool, auth_key: &XOnlyPublicKey) } } -pub async fn insert_new_deposit(pool: &sqlx::PgPool, token_id: &str, auth_key: &XOnlyPublicKey, server_public_key: &PublicKey, statechain_id: &String) { +pub async fn insert_new_deposit(pool: &sqlx::PgPool, token_id: &str, auth_key: &XOnlyPublicKey, server_public_key: &PublicKey, statechain_id: &String, enclave_index: i32) { - let query = "INSERT INTO statechain_data (token_id, auth_xonly_public_key, server_public_key, statechain_id) VALUES ($1, $2, $3, $4)"; + let query = "INSERT INTO statechain_data (token_id, auth_xonly_public_key, server_public_key, statechain_id, enclave_index) VALUES ($1, $2, $3, $4, $5)"; let _ = sqlx::query(query) .bind(token_id) .bind(&auth_key.serialize()) .bind(&server_public_key.serialize()) .bind(statechain_id) + .bind(enclave_index) .execute(pool) .await .unwrap(); diff --git a/server/src/database/mod.rs b/server/src/database/mod.rs index 1b064ecd..d116ffbd 100644 --- a/server/src/database/mod.rs +++ b/server/src/database/mod.rs @@ -2,3 +2,4 @@ pub mod transfer_sender; pub mod transfer_receiver; pub mod transfer; pub mod deposit; +pub mod utils; diff --git a/server/src/database/utils.rs b/server/src/database/utils.rs new file mode 100644 index 00000000..eaa1bfae --- /dev/null +++ b/server/src/database/utils.rs @@ -0,0 +1,24 @@ +use sqlx::Row; + +pub async fn get_enclave_index_from_database(pool: &sqlx::PgPool, statechain_id: &str) -> Option { + + let query = "SELECT enclave_index \ + FROM statechain_data \ + WHERE statechain_id = $1"; + + let row = sqlx::query(query) + .bind(statechain_id) + .fetch_optional(pool) + .await + .unwrap(); + + if row.is_none() { + return None; + } + + let row = row.unwrap(); + + let enclave_index: i32 = row.get("enclave_index"); + + Some(enclave_index) +} \ No newline at end of file diff --git a/server/src/endpoints/deposit.rs b/server/src/endpoints/deposit.rs index aff61f94..25ee6f74 100644 --- a/server/src/endpoints/deposit.rs +++ b/server/src/endpoints/deposit.rs @@ -1,16 +1,18 @@ use std::str::FromStr; -use bitcoin::hashes::sha256; +use bitcoin::hashes::{sha256, Hash}; use rocket::{serde::json::Json, response::status, State, http::Status}; use secp256k1_zkp::{XOnlyPublicKey, schnorr::Signature, Message, Secp256k1, PublicKey}; use serde::{Serialize, Deserialize}; use serde_json::{Value, json}; -use crate::server::StateChainEntity; +use crate::{server::StateChainEntity, server_config::Enclave}; #[get("/deposit/get_token")] pub async fn get_token(statechain_entity: &State) -> status::Custom> { - if statechain_entity.config.network == "mainnet" { + let config = crate::server_config::ServerConfig::load(); + + if config.network == "mainnet" { let response_body = json!({ "error": "Internal Server Error", "message": "Token generation not supported on mainnet." @@ -35,7 +37,9 @@ pub async fn get_token(statechain_entity: &State) -> status::C #[get("/tokens/token_init")] pub async fn token_init(statechain_entity: &State) -> status::Custom> { - if statechain_entity.config.network == "mainnet" { + let config = crate::server_config::ServerConfig::load(); + + if config.network == "mainnet" { let response_body = json!({ "error": "Internal Server Error", "message": "Token generation not supported on mainnet." @@ -71,6 +75,33 @@ pub async fn token_init(statechain_entity: &State) -> status:: return status::Custom(Status::Ok, Json(response_body)); } +fn get_random_enclave_index(statechain_id: &str, enclaves: &Vec) -> Result { + let index_from_statechain_id = get_enclave_index_from_statechain_id(statechain_id, enclaves.len() as u32); + + let selected_enclave = enclaves.get(index_from_statechain_id).unwrap(); + if selected_enclave.allow_deposit { + return Ok(index_from_statechain_id); + } else { + for (i, enclave) in enclaves.iter().enumerate() { + if enclave.allow_deposit { + return Ok(i); + } + } + } + + Err("No valid enclave found with allow_deposit set to true".to_string()) +} + +fn get_enclave_index_from_statechain_id(statechain_id: &str, enclave_array_len: u32) -> usize { + let hash = sha256::Hash::hash(statechain_id.as_bytes()); + let hash_bytes = hash.as_byte_array(); + let mut bytes = [0u8; 16]; + bytes.copy_from_slice(&hash_bytes[..16]); + let random_number = u128::from_be_bytes(bytes); + + return (random_number % enclave_array_len as u128) as usize; +} + #[post("/deposit/init/pod", format = "json", data = "")] pub async fn post_deposit(statechain_entity: &State, deposit_msg1: Json) -> status::Custom> { @@ -123,14 +154,18 @@ pub async fn post_deposit(statechain_entity: &State, deposit_m return status::Custom(Status::Gone, Json(response_body)); } - let statechain_id = uuid::Uuid::new_v4().as_simple().to_string(); - #[derive(Debug, Serialize, Deserialize)] pub struct GetPublicKeyRequestPayload { statechain_id: String, } - let lockbox_endpoint = statechain_entity.config.lockbox.clone().unwrap(); + let statechain_id = uuid::Uuid::new_v4().as_simple().to_string(); + + let config = crate::server_config::ServerConfig::load(); + + let enclave_index = get_random_enclave_index(&statechain_id, &config.enclaves).unwrap(); + + let lockbox_endpoint = config.enclaves.get(enclave_index).unwrap().url.clone(); let path = "get_public_key"; let client: reqwest::Client = reqwest::Client::new(); @@ -170,7 +205,7 @@ pub async fn post_deposit(statechain_entity: &State, deposit_m let server_pubkey = PublicKey::from_str(&server_pubkey_hex).unwrap(); - crate::database::deposit::insert_new_deposit(&statechain_entity.pool, &token_id, &auth_key, &server_pubkey, &statechain_id).await; + crate::database::deposit::insert_new_deposit(&statechain_entity.pool, &token_id, &auth_key, &server_pubkey, &statechain_id, enclave_index as i32).await; crate::database::deposit::set_token_spent(&statechain_entity.pool, &token_id).await; diff --git a/server/src/endpoints/mod.rs b/server/src/endpoints/mod.rs index 201bf810..0639d25e 100644 --- a/server/src/endpoints/mod.rs +++ b/server/src/endpoints/mod.rs @@ -1,7 +1,4 @@ use chrono::{DateTime, Duration, Utc}; -use rocket::State; - -use crate::server::StateChainEntity; pub mod deposit; pub mod sign; @@ -11,8 +8,11 @@ pub mod transfer_receiver; pub mod withdraw; -fn is_batch_expired(statechain_entity: &State, batch_time: DateTime) -> bool { - let batch_timeout = statechain_entity.config.batch_timeout; +fn is_batch_expired(batch_time: DateTime) -> bool { + + let config = crate::server_config::ServerConfig::load(); + + let batch_timeout = config.batch_timeout; let expiration_time = batch_time + Duration::seconds(batch_timeout as i64); diff --git a/server/src/endpoints/sign.rs b/server/src/endpoints/sign.rs index 7bbd004e..c0824cbe 100644 --- a/server/src/endpoints/sign.rs +++ b/server/src/endpoints/sign.rs @@ -48,15 +48,33 @@ pub async fn insert_new_signature_data(pool: &sqlx::PgPool, server_pubnonce: &st #[post("/sign/first", format = "json", data = "")] pub async fn sign_first(statechain_entity: &State, sign_first_request_payload: Json) -> status::Custom> { + let config = crate::server_config::ServerConfig::load(); + + let statechain_id = sign_first_request_payload.0.statechain_id.clone(); + let statechain_entity = statechain_entity.inner(); - let lockbox_endpoint = statechain_entity.config.lockbox.clone().unwrap(); + let enclave_index = crate::database::utils::get_enclave_index_from_database(&statechain_entity.pool, &statechain_id).await; + + let enclave_index = match enclave_index { + Some(index) => index, + None => { + let response_body = json!({ + "message": format!("Enclave index for statechain {} ID not found.", statechain_id) + }); + + return status::Custom(Status::InternalServerError, Json(response_body)); + } + }; + + let enclave_index = enclave_index as usize; + + let lockbox_endpoint = config.enclaves.get(enclave_index).unwrap().url.clone(); let path = "get_public_nonce"; let client: reqwest::Client = reqwest::Client::new(); let request = client.post(&format!("{}/{}", lockbox_endpoint, path)); - let statechain_id = sign_first_request_payload.0.statechain_id.clone(); let signed_statechain_id = sign_first_request_payload.0.signed_statechain_id.clone(); if !crate::endpoints::utils::validate_signature(&statechain_entity.pool, &signed_statechain_id, &statechain_id).await { @@ -128,15 +146,33 @@ pub async fn update_signature_data_challenge(pool: &sqlx::PgPool, server_pub_non #[post("/sign/second", format = "json", data = "")] pub async fn sign_second (statechain_entity: &State, partial_signature_request_payload: Json) -> status::Custom> { + let statechain_id = partial_signature_request_payload.0.statechain_id.clone(); + let statechain_entity = statechain_entity.inner(); - let lockbox_endpoint = statechain_entity.config.lockbox.clone().unwrap(); + let config = crate::server_config::ServerConfig::load(); + + let enclave_index = crate::database::utils::get_enclave_index_from_database(&statechain_entity.pool, &statechain_id).await; + + let enclave_index = match enclave_index { + Some(index) => index, + None => { + let response_body = json!({ + "message": format!("Enclave index for statechain {} ID not found.", statechain_id) + }); + + return status::Custom(Status::InternalServerError, Json(response_body)); + } + }; + + let enclave_index = enclave_index as usize; + + let lockbox_endpoint = config.enclaves.get(enclave_index).unwrap().url.clone(); let path = "get_partial_signature"; let client: reqwest::Client = reqwest::Client::new(); let request = client.post(&format!("{}/{}", lockbox_endpoint, path)); - let statechain_id = partial_signature_request_payload.0.statechain_id.clone(); let signed_statechain_id = partial_signature_request_payload.0.signed_statechain_id.clone(); if !crate::endpoints::utils::validate_signature(&statechain_entity.pool, &signed_statechain_id, &statechain_id).await { diff --git a/server/src/endpoints/transfer_receiver.rs b/server/src/endpoints/transfer_receiver.rs index ee50037e..331d5b9b 100644 --- a/server/src/endpoints/transfer_receiver.rs +++ b/server/src/endpoints/transfer_receiver.rs @@ -25,7 +25,24 @@ pub async fn statechain_info(statechain_entity: &State, statec let enclave_public_key = enclave_public_key.unwrap(); - let lockbox_endpoint = statechain_entity.config.lockbox.clone().unwrap(); + let config = crate::server_config::ServerConfig::load(); + + let enclave_index = crate::database::utils::get_enclave_index_from_database(&statechain_entity.pool, &statechain_id).await; + + let enclave_index = match enclave_index { + Some(index) => index, + None => { + let response_body = json!({ + "message": format!("Enclave index for statechain {} ID not found.", statechain_id) + }); + + return status::Custom(Status::InternalServerError, Json(response_body)); + } + }; + + let enclave_index = enclave_index as usize; + + let lockbox_endpoint = config.enclaves.get(enclave_index).unwrap().url.clone(); let path = "signature_count"; let client: reqwest::Client = reqwest::Client::new(); @@ -143,7 +160,7 @@ pub async fn validate_batch(statechain_entity: &State, statech let (batch_id, batch_time) = batch_info.unwrap(); - if is_batch_expired(&statechain_entity, batch_time) { + if is_batch_expired(batch_time) { // the batch time has not expired. It is possible to add a new coin to the batch. return BatchTransferReceiveValidationResult::ExpiredBatchTimeError("Batch time has expired".to_string()); } else { @@ -256,8 +273,24 @@ pub async fn transfer_receiver(statechain_entity: &State, tran x1: x1_hex, }; + let config = crate::server_config::ServerConfig::load(); + + let enclave_index = crate::database::utils::get_enclave_index_from_database(&statechain_entity.pool, &statechain_id).await; + + let enclave_index = match enclave_index { + Some(index) => index, + None => { + let response_body = json!({ + "message": format!("Enclave index for statechain {} ID not found.", statechain_id) + }); + + return status::Custom(Status::InternalServerError, Json(response_body)); + } + }; + + let enclave_index = enclave_index as usize; - let lockbox_endpoint = statechain_entity.config.lockbox.clone().unwrap(); + let lockbox_endpoint = config.enclaves.get(enclave_index).unwrap().url.clone(); let path = "keyupdate"; let client: reqwest::Client = reqwest::Client::new(); diff --git a/server/src/endpoints/transfer_sender.rs b/server/src/endpoints/transfer_sender.rs index d418710f..7436798c 100644 --- a/server/src/endpoints/transfer_sender.rs +++ b/server/src/endpoints/transfer_sender.rs @@ -30,7 +30,7 @@ pub async fn validate_batch_transfer(statechain_entity: &State let (batch_id, batch_time) = batch_info.unwrap(); - if !is_batch_expired(&statechain_entity, batch_time) { + if !is_batch_expired(batch_time) { // TODO: check if the batch is complete. If complete, should return success. @@ -59,7 +59,7 @@ pub async fn validate_batch_transfer(statechain_entity: &State if batch_time.is_some() { let batch_time = batch_time.unwrap(); - if !is_batch_expired(&statechain_entity, batch_time) { + if !is_batch_expired(batch_time) { // the batch time has not expired. It is possible to add a new coin to the batch. return BatchTransferValidationResult::Success } else { diff --git a/server/src/endpoints/utils.rs b/server/src/endpoints/utils.rs index d0b7fa5c..e4cd660d 100644 --- a/server/src/endpoints/utils.rs +++ b/server/src/endpoints/utils.rs @@ -1,13 +1,13 @@ use std::str::FromStr; -use bitcoin::hashes::sha256; +use bitcoin::hashes::{sha256, Hash}; use rocket::{State, response::status, http::Status, serde::json::Json}; use secp256k1_zkp::{schnorr::Signature, Message, Secp256k1, XOnlyPublicKey}; use serde_json::{json, Value}; use sqlx::Row; use secp256k1_zkp::PublicKey; -use crate::server::StateChainEntity; +use crate::{server::StateChainEntity, server_config::Enclave}; async fn get_auth_key_by_statechain_id(pool: &sqlx::PgPool, statechain_id: &str) -> Result { @@ -55,12 +55,13 @@ pub async fn validate_signature(pool: &sqlx::PgPool, signed_message_hex: &str, s } #[get("/info/config")] -pub async fn info_config(statechain_entity: &State) -> status::Custom> { - let statechain_entity = statechain_entity.inner(); +pub async fn info_config() -> status::Custom> { + + let config = crate::server_config::ServerConfig::load(); let server_config = mercurylib::utils::ServerConfig { - initlock: statechain_entity.config.lockheight_init, - interval: statechain_entity.config.lh_decrement, + initlock: config.lockheight_init, + interval: config.lh_decrement, }; let response_body = json!(server_config); @@ -124,4 +125,4 @@ pub async fn info_keylist(statechain_entity: &State) -> status return status::Custom(Status::Ok, Json(response_body)); -} \ No newline at end of file +} diff --git a/server/src/endpoints/withdraw.rs b/server/src/endpoints/withdraw.rs index 661717cb..a26fd9a0 100644 --- a/server/src/endpoints/withdraw.rs +++ b/server/src/endpoints/withdraw.rs @@ -43,7 +43,24 @@ pub async fn withdraw_complete(statechain_entity: &State, dele return status::Custom(Status::InternalServerError, Json(response_body)); } - let lockbox_endpoint = statechain_entity.config.lockbox.clone().unwrap(); + let config = crate::server_config::ServerConfig::load(); + + let enclave_index = crate::database::utils::get_enclave_index_from_database(&statechain_entity.pool, &statechain_id).await; + + let enclave_index = match enclave_index { + Some(index) => index, + None => { + let response_body = json!({ + "message": format!("Enclave index for statechain {} ID not found.", statechain_id) + }); + + return status::Custom(Status::InternalServerError, Json(response_body)); + } + }; + + let enclave_index = enclave_index as usize; + + let lockbox_endpoint = config.enclaves.get(enclave_index).unwrap().url.clone(); let path = "delete_statechain"; let client: reqwest::Client = reqwest::Client::new(); diff --git a/server/src/server.rs b/server/src/server.rs index c11b1cf2..bf41794c 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -5,7 +5,6 @@ use sqlx::{Pool, Postgres, postgres::PgPoolOptions}; use crate::server_config::ServerConfig; pub struct StateChainEntity { - pub config: ServerConfig, pub pool: Pool, } @@ -13,7 +12,7 @@ impl StateChainEntity { pub async fn new() -> Self { let config = ServerConfig::load(); - + let pool = PgPoolOptions::new() .max_connections(10) @@ -23,7 +22,6 @@ impl StateChainEntity { .unwrap(); StateChainEntity { - config, pool, } } diff --git a/server/src/server_config.rs b/server/src/server_config.rs index 08c7275b..b10f7dea 100644 --- a/server/src/server_config.rs +++ b/server/src/server_config.rs @@ -2,11 +2,15 @@ use config::{Config as ConfigRs, File}; use serde::{Serialize, Deserialize}; use std::env; +#[derive(Debug, Serialize, Deserialize)] +pub struct Enclave { + pub url: String, + pub allow_deposit: bool, +} + /// Config struct storing all StataChain Entity config #[derive(Debug, Serialize, Deserialize)] pub struct ServerConfig { - /// Active lockbox server addresses - pub lockbox: Option, /// Bitcoin network name (testnet, regtest, mainnet) pub network: String, /// Initial deposit backup nlocktime @@ -17,17 +21,28 @@ pub struct ServerConfig { pub connection_string: String, /// Batch timeout pub batch_timeout: u32, + /// Enclave server list + pub enclaves: Vec, } impl Default for ServerConfig { fn default() -> ServerConfig { ServerConfig { - lockbox: None, network: String::from("regtest"), lockheight_init: 10000, lh_decrement: 100, connection_string: String::from("postgres://postgres:postgres@db_server:5432/mercury"), batch_timeout: 120, + enclaves: vec![ + Enclave { + url: "http://0.0.0.0:18080".to_string(), + allow_deposit: true, + }, + Enclave { + url: "http://0.0.0.0:18080".to_string(), + allow_deposit: false, + } + ], } } } @@ -35,12 +50,12 @@ impl Default for ServerConfig { impl From for ServerConfig { fn from(config: ConfigRs) -> Self { ServerConfig { - lockbox: config.get::>("lockbox").unwrap_or(None), network: config.get::("network").unwrap_or_else(|_| String::new()), lockheight_init: config.get::("lockheight_init").unwrap_or(0), lh_decrement: config.get::("lh_decrement").unwrap_or(0), connection_string: config.get::("connection_string").unwrap_or_else(|_| String::new()), batch_timeout: config.get::("batch_timeout").unwrap_or(0), + enclaves: config.get::>("enclaves").unwrap_or_else(|_| Vec::new()), } } } @@ -66,13 +81,26 @@ impl ServerConfig { env::var(env_var).unwrap_or_else(|_| settings.get_string(key).unwrap()) }; + let get_env_or_config_enclave = |key: &str, env_var: &str| -> Vec { + + let env_enclaves = env::var(env_var); + + + if env_enclaves.is_ok() { + + return serde_json::from_str::>(&env_enclaves.unwrap()).unwrap(); + } + + settings.get::>(key).unwrap() + }; + ServerConfig { - lockbox: Some(get_env_or_config("lockbox", "LOCKBOX_URL")), network: get_env_or_config("network", "BITCOIN_NETWORK"), lockheight_init: get_env_or_config("lockheight_init", "LOCKHEIGHT_INIT").parse::().unwrap(), lh_decrement: get_env_or_config("lh_decrement", "LH_DECREMENT").parse::().unwrap(), connection_string: get_env_or_config("connection_string", "CONNECTION_STRING"), batch_timeout: get_env_or_config("batch_timeout", "BATCH_TIMEOUT").parse::().unwrap(), + enclaves: get_env_or_config_enclave("enclaves", "ENCLAVES"), } } -} \ No newline at end of file +}