diff --git a/fhevm-engine/coprocessor/migrations/20240723111257_coprocessor.sql b/fhevm-engine/coprocessor/migrations/20240723111257_coprocessor.sql index 3c1e4f23..82402778 100644 --- a/fhevm-engine/coprocessor/migrations/20240723111257_coprocessor.sql +++ b/fhevm-engine/coprocessor/migrations/20240723111257_coprocessor.sql @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3533155bc13f8aeea5994e7f82a8f5b1100115f22cb29e6533474529c8b866f4 +oid sha256:6d7478de3eaed5c9c897baea903efad12b3860fd40e9b0704e06f6c281cbccbe size 530459451 diff --git a/fhevm-engine/coprocessor/migrations/gen-keys.sh b/fhevm-engine/coprocessor/migrations/gen-keys.sh index b3770495..21a4f485 100755 --- a/fhevm-engine/coprocessor/migrations/gen-keys.sh +++ b/fhevm-engine/coprocessor/migrations/gen-keys.sh @@ -4,8 +4,8 @@ echo " INSERT INTO tenants(tenant_api_key, pks_key, sks_key, cks_key) VALUES ( 'a1503fb6-d79b-4e9e-826d-44cf262f3e05', - decode('$(cat ../fhevm-keys/pks | xxd -p | tr -d '\n')','hex'), - decode('$(cat ../fhevm-keys/sks | xxd -p | tr -d '\n')','hex'), - decode('$(cat ../fhevm-keys/cks | xxd -p | tr -d '\n')','hex') + decode('$(cat ../../fhevm-keys/pks | xxd -p | tr -d '\n')','hex'), + decode('$(cat ../../fhevm-keys/sks | xxd -p | tr -d '\n')','hex'), + decode('$(cat ../../fhevm-keys/cks | xxd -p | tr -d '\n')','hex') ) " > 20240723111257_coprocessor.sql diff --git a/fhevm-engine/coprocessor/src/server.rs b/fhevm-engine/coprocessor/src/server.rs index 55ea2bcf..ae75bd16 100644 --- a/fhevm-engine/coprocessor/src/server.rs +++ b/fhevm-engine/coprocessor/src/server.rs @@ -63,143 +63,12 @@ pub async fn run_server( #[tonic::async_trait] impl coprocessor::fhevm_coprocessor_server::FhevmCoprocessor for CoprocessorService { - async fn debug_encrypt_ciphertext( - &self, - request: tonic::Request, - ) -> std::result::Result, tonic::Status> { - let tenant_id = check_if_api_key_is_valid(&request, &self.pool).await?; - let req = request.get_ref(); - - let mut public_key = sqlx::query!( - " - SELECT sks_key - FROM tenants - WHERE tenant_id = $1 - ", - tenant_id - ) - .fetch_all(&self.pool) - .await - .map_err(Into::::into)?; - - assert_eq!(public_key.len(), 1); - - let public_key = public_key.pop().unwrap(); - - let cloned = req.values.clone(); - let out_cts = tokio::task::spawn_blocking(move || { - let server_key: tfhe::ServerKey = bincode::deserialize(&public_key.sks_key).unwrap(); - tfhe::set_server_key(server_key); - - // single threaded implementation as this is debug function and it is simple to implement - let mut res: Vec<(Vec, i16, Vec)> = Vec::with_capacity(cloned.len()); - for v in cloned { - let ct = debug_trivial_encrypt_be_bytes(v.output_type as i16, &v.le_value); - let (ct_type, ct_bytes) = ct.serialize(); - res.push((v.handle, ct_type, ct_bytes)); - } - - res - }) - .await - .unwrap(); - - let mut conn = self - .pool - .acquire() - .await - .map_err(Into::::into)?; - let mut trx = conn.begin().await.map_err(Into::::into)?; - - for (handle, db_type, db_bytes) in out_cts { - sqlx::query!(" - INSERT INTO ciphertexts(tenant_id, handle, ciphertext, ciphertext_version, ciphertext_type) - VALUES ($1, $2, $3, $4, $5) - ", - tenant_id, handle, db_bytes, current_ciphertext_version(), db_type as i16 - ) - .execute(trx.as_mut()).await.map_err(Into::::into)?; - } - - trx.commit().await.map_err(Into::::into)?; - - return Ok(tonic::Response::new(GenericResponse { response_code: 0 })); - } - - async fn debug_decrypt_ciphertext( - &self, - request: tonic::Request, - ) -> std::result::Result, tonic::Status> - { - let tenant_id = check_if_api_key_is_valid(&request, &self.pool).await?; - let req = request.get_ref(); - - let mut priv_key = sqlx::query!( - " - SELECT cks_key - FROM tenants - WHERE tenant_id = $1 - ", - tenant_id - ) - .fetch_all(&self.pool) - .await - .map_err(Into::::into)?; - - if priv_key.is_empty() || priv_key[0].cks_key.is_none() { - return Err(tonic::Status::not_found("tenant private key not found")); - } - - assert_eq!(priv_key.len(), 1); - - let cts = sqlx::query!( - " - SELECT ciphertext, ciphertext_type, handle - FROM ciphertexts - WHERE tenant_id = $1 - AND handle = ANY($2::BYTEA[]) - AND ciphertext_version = $3 - ", - tenant_id, - &req.handles, - current_ciphertext_version() - ) - .fetch_all(&self.pool) - .await - .map_err(Into::::into)?; - - if cts.is_empty() { - return Err(tonic::Status::not_found("ciphertext not found")); - } - - let priv_key = priv_key.pop().unwrap().cks_key.unwrap(); - - let values = tokio::task::spawn_blocking(move || { - let client_key: tfhe::ClientKey = bincode::deserialize(&priv_key).unwrap(); - - let mut decrypted: Vec = Vec::with_capacity(cts.len()); - for ct in cts { - let deserialized = - deserialize_fhe_ciphertext(ct.ciphertext_type, &ct.ciphertext) - .unwrap(); - decrypted.push(DebugDecryptResponseSingle { - output_type: ct.ciphertext_type as i32, - value: deserialized.decrypt(&client_key), - }); - } - - decrypted - }) - .await - .unwrap(); - - return Ok(tonic::Response::new(DebugDecryptResponse { values })); - } - async fn upload_inputs( &self, request: tonic::Request, ) -> std::result::Result, tonic::Status> { + let tenant_id = check_if_api_key_is_valid(&request, &self.pool).await?; + let req = request.get_ref(); if req.input_ciphertexts.len() > self.args.maximimum_compact_inputs_upload { return Err(tonic::Status::from_error(Box::new( @@ -217,8 +86,6 @@ impl coprocessor::fhevm_coprocessor_server::FhevmCoprocessor for CoprocessorServ return Ok(tonic::Response::new(response)); } - let tenant_id = check_if_api_key_is_valid(&request, &self.pool).await?; - let server_key = { fetch_tenant_server_key(tenant_id, &self.pool, &self.tenant_key_cache) .await @@ -335,42 +202,6 @@ impl coprocessor::fhevm_coprocessor_server::FhevmCoprocessor for CoprocessorServ Ok(tonic::Response::new(response)) } - async fn upload_ciphertexts( - &self, - request: tonic::Request, - ) -> std::result::Result, tonic::Status> { - let tenant_id = check_if_api_key_is_valid(&request, &self.pool).await?; - - let req = request.get_ref(); - - // TODO: check if ciphertext deserializes into type correctly - // TODO: check for duplicate handles in the input - // TODO: check if ciphertext doesn't exist already - // TODO: if ciphertexts exists check that it is equal to the one being uploaded - - let mut trx = self - .pool - .begin() - .await - .map_err(Into::::into)?; - for i_ct in &req.input_ciphertexts { - let ciphertext_type: i16 = i_ct - .ciphertext_type - .try_into() - .map_err(|_e| CoprocessorError::FhevmError(FhevmError::UnknownFheType(i_ct.ciphertext_type)))?; - let _ = sqlx::query!(" - INSERT INTO ciphertexts(tenant_id, handle, ciphertext, ciphertext_version, ciphertext_type) - VALUES($1, $2, $3, $4, $5) - ON CONFLICT (tenant_id, handle, ciphertext_version) DO NOTHING - ", tenant_id, i_ct.ciphertext_handle, i_ct.ciphertext_bytes, current_ciphertext_version(), ciphertext_type) - .execute(trx.as_mut()).await.map_err(Into::::into)?; - } - - trx.commit().await.map_err(Into::::into)?; - - return Ok(tonic::Response::new(GenericResponse { response_code: 0 })); - } - async fn async_compute( &self, request: tonic::Request, @@ -504,4 +335,175 @@ impl coprocessor::fhevm_coprocessor_server::FhevmCoprocessor for CoprocessorServ ) -> std::result::Result, tonic::Status> { return Err(tonic::Status::unimplemented("not implemented")); } + + // debug functions below, should be removed in production + async fn debug_encrypt_ciphertext( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status> { + let tenant_id = check_if_api_key_is_valid(&request, &self.pool).await?; + let req = request.get_ref(); + + let mut public_key = sqlx::query!( + " + SELECT sks_key + FROM tenants + WHERE tenant_id = $1 + ", + tenant_id + ) + .fetch_all(&self.pool) + .await + .map_err(Into::::into)?; + + assert_eq!(public_key.len(), 1); + + let public_key = public_key.pop().unwrap(); + + let cloned = req.values.clone(); + let out_cts = tokio::task::spawn_blocking(move || { + let server_key: tfhe::ServerKey = bincode::deserialize(&public_key.sks_key).unwrap(); + tfhe::set_server_key(server_key); + + // single threaded implementation as this is debug function and it is simple to implement + let mut res: Vec<(Vec, i16, Vec)> = Vec::with_capacity(cloned.len()); + for v in cloned { + let ct = debug_trivial_encrypt_be_bytes(v.output_type as i16, &v.le_value); + let (ct_type, ct_bytes) = ct.serialize(); + res.push((v.handle, ct_type, ct_bytes)); + } + + res + }) + .await + .unwrap(); + + let mut conn = self + .pool + .acquire() + .await + .map_err(Into::::into)?; + let mut trx = conn.begin().await.map_err(Into::::into)?; + + for (handle, db_type, db_bytes) in out_cts { + sqlx::query!(" + INSERT INTO ciphertexts(tenant_id, handle, ciphertext, ciphertext_version, ciphertext_type) + VALUES ($1, $2, $3, $4, $5) + ", + tenant_id, handle, db_bytes, current_ciphertext_version(), db_type as i16 + ) + .execute(trx.as_mut()).await.map_err(Into::::into)?; + } + + trx.commit().await.map_err(Into::::into)?; + + return Ok(tonic::Response::new(GenericResponse { response_code: 0 })); + } + + async fn debug_decrypt_ciphertext( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status> + { + let tenant_id = check_if_api_key_is_valid(&request, &self.pool).await?; + let req = request.get_ref(); + + let mut priv_key = sqlx::query!( + " + SELECT cks_key + FROM tenants + WHERE tenant_id = $1 + ", + tenant_id + ) + .fetch_all(&self.pool) + .await + .map_err(Into::::into)?; + + if priv_key.is_empty() || priv_key[0].cks_key.is_none() { + return Err(tonic::Status::not_found("tenant private key not found")); + } + + assert_eq!(priv_key.len(), 1); + + let cts = sqlx::query!( + " + SELECT ciphertext, ciphertext_type, handle + FROM ciphertexts + WHERE tenant_id = $1 + AND handle = ANY($2::BYTEA[]) + AND ciphertext_version = $3 + ", + tenant_id, + &req.handles, + current_ciphertext_version() + ) + .fetch_all(&self.pool) + .await + .map_err(Into::::into)?; + + if cts.is_empty() { + return Err(tonic::Status::not_found("ciphertext not found")); + } + + let priv_key = priv_key.pop().unwrap().cks_key.unwrap(); + + let values = tokio::task::spawn_blocking(move || { + let client_key: tfhe::ClientKey = bincode::deserialize(&priv_key).unwrap(); + + let mut decrypted: Vec = Vec::with_capacity(cts.len()); + for ct in cts { + let deserialized = + deserialize_fhe_ciphertext(ct.ciphertext_type, &ct.ciphertext) + .unwrap(); + decrypted.push(DebugDecryptResponseSingle { + output_type: ct.ciphertext_type as i32, + value: deserialized.decrypt(&client_key), + }); + } + + decrypted + }) + .await + .unwrap(); + + return Ok(tonic::Response::new(DebugDecryptResponse { values })); + } + + async fn upload_ciphertexts( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status> { + let tenant_id = check_if_api_key_is_valid(&request, &self.pool).await?; + + let req = request.get_ref(); + + // TODO: check if ciphertext deserializes into type correctly + // TODO: check for duplicate handles in the input + // TODO: check if ciphertext doesn't exist already + // TODO: if ciphertexts exists check that it is equal to the one being uploaded + + let mut trx = self + .pool + .begin() + .await + .map_err(Into::::into)?; + for i_ct in &req.input_ciphertexts { + let ciphertext_type: i16 = i_ct + .ciphertext_type + .try_into() + .map_err(|_e| CoprocessorError::FhevmError(FhevmError::UnknownFheType(i_ct.ciphertext_type)))?; + let _ = sqlx::query!(" + INSERT INTO ciphertexts(tenant_id, handle, ciphertext, ciphertext_version, ciphertext_type) + VALUES($1, $2, $3, $4, $5) + ON CONFLICT (tenant_id, handle, ciphertext_version) DO NOTHING + ", tenant_id, i_ct.ciphertext_handle, i_ct.ciphertext_bytes, current_ciphertext_version(), ciphertext_type) + .execute(trx.as_mut()).await.map_err(Into::::into)?; + } + + trx.commit().await.map_err(Into::::into)?; + + return Ok(tonic::Response::new(GenericResponse { response_code: 0 })); + } + } diff --git a/fhevm-engine/coprocessor/src/tests/errors.rs b/fhevm-engine/coprocessor/src/tests/errors.rs new file mode 100644 index 00000000..c1e8f98e --- /dev/null +++ b/fhevm-engine/coprocessor/src/tests/errors.rs @@ -0,0 +1,622 @@ +use std::str::FromStr; + +use tonic::metadata::MetadataValue; +use crate::{db_queries::query_tenant_keys, server::{common::FheOperation, coprocessor::{async_computation_input::Input, fhevm_coprocessor_client::FhevmCoprocessorClient, AsyncComputation, AsyncComputationInput, AsyncComputeRequest, InputToUpload, InputUploadBatch}}, tests::utils::{default_api_key, default_tenant_id, setup_test_app}}; + +#[tokio::test] +async fn test_coprocessor_input_errors() -> Result<(), Box> { + let app = setup_test_app().await?; + let mut client = FhevmCoprocessorClient::connect(app.app_url().to_string()).await?; + let api_key_header = format!("bearer {}", default_api_key()); + let pool = sqlx::postgres::PgPoolOptions::new() + .max_connections(2) + .connect(app.db_url()) + .await?; + + let keys = query_tenant_keys(vec![default_tenant_id()], &pool).await.map_err(|e| { + let e: Box = e; + e + })?; + let keys = &keys[0]; + + { // too many uploads at once + let mut builder = tfhe::CompactCiphertextListBuilder::new(&keys.pks); + let the_list = builder + .push(false) + .push(1u8) + .push(2u16) + .push(3u32) + .push(4u64) + .build(); + + let serialized = bincode::serialize(&the_list).unwrap(); + + let mut input_ciphertexts = Vec::new(); + for _ in 0..12 { + input_ciphertexts.push(InputToUpload { + input_payload: serialized.clone(), + signature: Vec::new(), + }); + } + + println!("Encrypting inputs..."); + let mut input_request = tonic::Request::new(InputUploadBatch { + input_ciphertexts, + }); + input_request.metadata_mut().append( + "authorization", + MetadataValue::from_str(&api_key_header).unwrap(), + ); + let resp = client.upload_inputs(input_request).await; + match resp { + Err(e) => { + assert!(e.to_string().contains("More than maximum input blobs uploaded, maximum allowed: 10, uploaded: 12")); + } + Ok(_) => { + panic!("Should not have succeeded") + } + } + } + + { // garbage ciphertext + let mut builder = tfhe::CompactCiphertextListBuilder::new(&keys.pks); + let the_list = builder + .push(false) + .push(1u8) + .push(2u16) + .push(3u32) + .push(4u64) + .build(); + + let serialized = bincode::serialize(&the_list).unwrap(); + + let mut input_ciphertexts = Vec::new(); + input_ciphertexts.push(InputToUpload { + input_payload: serialized[0..32].to_vec(), + signature: Vec::new(), + }); + + println!("Encrypting inputs..."); + let mut input_request = tonic::Request::new(InputUploadBatch { + input_ciphertexts, + }); + input_request.metadata_mut().append( + "authorization", + MetadataValue::from_str(&api_key_header).unwrap(), + ); + let resp = client.upload_inputs(input_request).await; + match resp { + Err(e) => { + assert!(e.to_string().contains("error deserializing ciphertext")); + } + Ok(_) => { + panic!("Should not have succeeded") + } + } + } + + { // more ciphertexts than limit + let mut builder = tfhe::CompactCiphertextListBuilder::new(&keys.pks); + for _ in 0..300 { + let _ = builder.push(false); + } + + let the_list = builder.build(); + let serialized = bincode::serialize(&the_list).unwrap(); + + let mut input_ciphertexts = Vec::new(); + input_ciphertexts.push(InputToUpload { + input_payload: serialized, + signature: Vec::new(), + }); + + println!("Encrypting inputs..."); + let mut input_request = tonic::Request::new(InputUploadBatch { + input_ciphertexts, + }); + input_request.metadata_mut().append( + "authorization", + MetadataValue::from_str(&api_key_header).unwrap(), + ); + let resp = client.upload_inputs(input_request).await; + match resp { + Err(e) => { + eprintln!("error: {e}"); + assert!(e.to_string().contains("Input blob contains too many ciphertexts")); + } + Ok(_) => { + panic!("Should not have succeeded") + } + } + } + + { // empty payload ok + let input_ciphertexts = Vec::new(); + + println!("Encrypting inputs..."); + let mut input_request = tonic::Request::new(InputUploadBatch { + input_ciphertexts, + }); + input_request.metadata_mut().append( + "authorization", + MetadataValue::from_str(&api_key_header).unwrap(), + ); + let resp = client.upload_inputs(input_request).await; + match resp { + Ok(_) => {} + Err(e) => { + panic!("unexpected error: {e}") + } + } + } + + Ok(()) +} + +#[tokio::test] +async fn test_coprocessor_api_key_errors() -> Result<(), Box> { + let app = setup_test_app().await?; + let mut client = FhevmCoprocessorClient::connect(app.app_url().to_string()).await?; + + { // not provided api key + println!("Encrypting inputs..."); + let input_request = tonic::Request::new(InputUploadBatch { + input_ciphertexts: Vec::new(), + }); + let resp = client.upload_inputs(input_request).await; + match resp { + Err(e) => { + assert!(e.to_string().contains("API key unknown/invalid/not provided")); + } + Ok(_) => { + panic!("Should not have succeeded") + } + } + } + + { // invalid api key + println!("Encrypting inputs..."); + let mut input_request = tonic::Request::new(InputUploadBatch { + input_ciphertexts: Vec::new(), + }); + let api_key_header = format!("bearer invalid-guid"); + input_request.metadata_mut().append( + "authorization", + MetadataValue::from_str(&api_key_header).unwrap(), + ); + let resp = client.upload_inputs(input_request).await; + match resp { + Err(e) => { + assert!(e.to_string().contains("API key unknown/invalid/not provided")); + } + Ok(_) => { + panic!("Should not have succeeded") + } + } + } + + { // non existing + println!("Encrypting inputs..."); + let mut input_request = tonic::Request::new(InputUploadBatch { + input_ciphertexts: Vec::new(), + }); + + let api_key_header = format!("bearer 9a671665-3842-400f-b4d1-37e194e5e9a0"); + input_request.metadata_mut().append( + "authorization", + MetadataValue::from_str(&api_key_header).unwrap(), + ); + let resp = client.upload_inputs(input_request).await; + match resp { + Err(e) => { + assert!(e.to_string().contains("API key unknown/invalid/not provided")); + } + Ok(_) => { + panic!("Should not have succeeded") + } + } + } + + Ok(()) +} + +#[tokio::test] +async fn test_coprocessor_computation_errors() -> Result<(), Box> { + let app = setup_test_app().await?; + let mut client = FhevmCoprocessorClient::connect(app.app_url().to_string()).await?; + let api_key_header = format!("bearer {}", default_api_key()); + let pool = sqlx::postgres::PgPoolOptions::new() + .max_connections(2) + .connect(app.db_url()) + .await?; + + let keys = query_tenant_keys(vec![default_tenant_id()], &pool).await.map_err(|e| { + let e: Box = e; + e + })?; + let keys = &keys[0]; + + let mut handle_counter = 0; + let mut next_handle = || { + let out: i32 = handle_counter; + handle_counter += 1; + out.to_be_bytes().to_vec() + }; + + let initial_inputs_resp = { // not provided api key + let mut builder = tfhe::CompactCiphertextListBuilder::new(&keys.pks); + let the_list = builder + .push(false) + .push(1u8) + .push(2u16) + .push(3u32) + .push(4u64) + .build(); + + let serialized = bincode::serialize(&the_list).unwrap(); + + let mut input_ciphertexts = Vec::new(); + input_ciphertexts.push(InputToUpload { + input_payload: serialized, + signature: Vec::new(), + }); + + println!("Encrypting inputs..."); + let mut input_request = tonic::Request::new(InputUploadBatch { + input_ciphertexts, + }); + input_request.metadata_mut().append( + "authorization", + MetadataValue::from_str(&api_key_header).unwrap(), + ); + client.upload_inputs(input_request).await? + }; + + let ct_vec = &initial_inputs_resp.get_ref().upload_responses; + assert_eq!(ct_vec.len(), 1); + let handles = &ct_vec[0].input_handles; + assert_eq!(handles.len(), 5); + let test_bool = &handles[0]; + let test_u8 = &handles[1]; + let test_u16 = &handles[2]; + let test_u32 = &handles[3]; + let test_u64 = &handles[4]; + + { // test circular dependencies + let output_handle_a = next_handle(); + let output_handle_b = next_handle(); + let output_handle_c = next_handle(); + // make circular dependency wheel + // a depends on c + // c depends on b + // b depends on a + let async_computations = vec![ + AsyncComputation { + operation: FheOperation::FheAdd.into(), + output_handle: output_handle_a.clone(), + inputs: vec![ + AsyncComputationInput { + input: Some(Input::InputHandle(test_u8.handle.clone())), + }, + AsyncComputationInput { + input: Some(Input::InputHandle(output_handle_c.clone())), + }, + ], + }, + AsyncComputation { + operation: FheOperation::FheAdd.into(), + output_handle: output_handle_b.clone(), + inputs: vec![ + AsyncComputationInput { + input: Some(Input::InputHandle(test_u8.handle.clone())), + }, + AsyncComputationInput { + input: Some(Input::InputHandle(output_handle_a.clone())), + }, + ], + }, + AsyncComputation { + operation: FheOperation::FheAdd.into(), + output_handle: output_handle_c.clone(), + inputs: vec![ + AsyncComputationInput { + input: Some(Input::InputHandle(test_u8.handle.clone())), + }, + AsyncComputationInput { + input: Some(Input::InputHandle(output_handle_b.clone())), + }, + ], + }, + ]; + let mut input_request = tonic::Request::new(AsyncComputeRequest { + computations: async_computations, + }); + input_request.metadata_mut().append( + "authorization", + MetadataValue::from_str(&api_key_header).unwrap(), + ); + match client.async_compute(input_request).await { + Ok(_) => { + panic!("Expected failure") + } + Err(e) => { + eprintln!("error: {}", e); + assert!(e.to_string().contains("has circular dependency and is uncomputable")); + } + } + } + + { // test invalid binary op between uncast types + let output_handle_a = next_handle(); + let async_computations = vec![ + AsyncComputation { + operation: FheOperation::FheAdd.into(), + output_handle: output_handle_a.clone(), + inputs: vec![ + AsyncComputationInput { + input: Some(Input::InputHandle(test_u8.handle.clone())), + }, + AsyncComputationInput { + input: Some(Input::InputHandle(test_u16.handle.clone())), + }, + ], + }, + ]; + let mut input_request = tonic::Request::new(AsyncComputeRequest { + computations: async_computations, + }); + input_request.metadata_mut().append( + "authorization", + MetadataValue::from_str(&api_key_header).unwrap(), + ); + match client.async_compute(input_request).await { + Ok(_) => { + panic!("Expected failure") + } + Err(e) => { + eprintln!("error: {}", e); + assert!(e.to_string().contains("fhevm error: FheOperationDoesntHaveUniformTypesAsInput")); + } + } + } + + { // empty ciphertext handle + let output_handle_a = next_handle(); + let async_computations = vec![ + AsyncComputation { + operation: FheOperation::FheAdd.into(), + output_handle: output_handle_a.clone(), + inputs: vec![ + AsyncComputationInput { + input: Some(Input::InputHandle(test_u32.handle.clone())), + }, + AsyncComputationInput { + input: Some(Input::InputHandle(vec![])), + }, + ], + }, + ]; + let mut input_request = tonic::Request::new(AsyncComputeRequest { + computations: async_computations, + }); + input_request.metadata_mut().append( + "authorization", + MetadataValue::from_str(&api_key_header).unwrap(), + ); + match client.async_compute(input_request).await { + Ok(_) => { + panic!("Expected failure") + } + Err(e) => { + eprintln!("error: {}", e); + assert!(e.to_string().contains("Found ciphertext handle is empty")); + } + } + } + + { // ciphertext handle too long + let output_handle_a = next_handle(); + let async_computations = vec![ + AsyncComputation { + operation: FheOperation::FheAdd.into(), + output_handle: output_handle_a.clone(), + inputs: vec![ + AsyncComputationInput { + input: Some(Input::InputHandle(test_u32.handle.clone())), + }, + AsyncComputationInput { + input: Some(Input::InputHandle(vec![0; 65])), + }, + ], + }, + ]; + let mut input_request = tonic::Request::new(AsyncComputeRequest { + computations: async_computations, + }); + input_request.metadata_mut().append( + "authorization", + MetadataValue::from_str(&api_key_header).unwrap(), + ); + match client.async_compute(input_request).await { + Ok(_) => { + panic!("Expected failure") + } + Err(e) => { + eprintln!("error: {}", e); + assert!(e.to_string().contains("Found ciphertext handle longer than 64 bytes")); + } + } + } + + { // computation too many inputs + let output_handle_a = next_handle(); + let async_computations = vec![ + AsyncComputation { + operation: FheOperation::FheAdd.into(), + output_handle: output_handle_a.clone(), + inputs: vec![ + AsyncComputationInput { + input: Some(Input::InputHandle(test_u64.handle.clone())), + }, + AsyncComputationInput { + input: Some(Input::InputHandle(test_u64.handle.clone())), + }, + AsyncComputationInput { + input: Some(Input::InputHandle(test_u64.handle.clone())), + }, + ], + }, + ]; + let mut input_request = tonic::Request::new(AsyncComputeRequest { + computations: async_computations, + }); + input_request.metadata_mut().append( + "authorization", + MetadataValue::from_str(&api_key_header).unwrap(), + ); + match client.async_compute(input_request).await { + Ok(_) => { + panic!("Expected failure") + } + Err(e) => { + eprintln!("error: {}", e); + assert!(e.to_string().contains("fhevm error: UnexpectedOperandCountForFheOperation")); + } + } + } + + { // scalar operand on the left + let output_handle_a = next_handle(); + let async_computations = vec![ + AsyncComputation { + operation: FheOperation::FheAdd.into(), + output_handle: output_handle_a.clone(), + inputs: vec![ + AsyncComputationInput { + input: Some(Input::Scalar(vec![123])), + }, + AsyncComputationInput { + input: Some(Input::InputHandle(test_u64.handle.clone())), + }, + ], + }, + ]; + let mut input_request = tonic::Request::new(AsyncComputeRequest { + computations: async_computations, + }); + input_request.metadata_mut().append( + "authorization", + MetadataValue::from_str(&api_key_header).unwrap(), + ); + match client.async_compute(input_request).await { + Ok(_) => { + panic!("Expected failure") + } + Err(e) => { + eprintln!("error: {}", e); + assert!(e.to_string().contains("fhevm error: FheOperationOnlySecondOperandCanBeScalar")); + } + } + } + + { // scalar division by zero + let output_handle_a = next_handle(); + let async_computations = vec![ + AsyncComputation { + operation: FheOperation::FheDiv.into(), + output_handle: output_handle_a.clone(), + inputs: vec![ + AsyncComputationInput { + input: Some(Input::InputHandle(test_u64.handle.clone())), + }, + AsyncComputationInput { + input: Some(Input::Scalar(vec![0])), + }, + ], + }, + ]; + let mut input_request = tonic::Request::new(AsyncComputeRequest { + computations: async_computations, + }); + input_request.metadata_mut().append( + "authorization", + MetadataValue::from_str(&api_key_header).unwrap(), + ); + match client.async_compute(input_request).await { + Ok(_) => { + panic!("Expected failure") + } + Err(e) => { + eprintln!("error: {}", e); + assert!(e.to_string().contains("fhevm error: FheOperationScalarDivisionByZero")); + } + } + } + + { // binary boolean inputs + let output_handle_a = next_handle(); + let async_computations = vec![ + AsyncComputation { + operation: FheOperation::FheAdd.into(), + output_handle: output_handle_a.clone(), + inputs: vec![ + AsyncComputationInput { + input: Some(Input::InputHandle(test_bool.handle.clone())), + }, + AsyncComputationInput { + input: Some(Input::InputHandle(test_bool.handle.clone())), + }, + ], + }, + ]; + let mut input_request = tonic::Request::new(AsyncComputeRequest { + computations: async_computations, + }); + input_request.metadata_mut().append( + "authorization", + MetadataValue::from_str(&api_key_header).unwrap(), + ); + match client.async_compute(input_request).await { + Ok(_) => { + panic!("Expected failure") + } + Err(e) => { + eprintln!("error: {}", e); + assert!(e.to_string().contains("fhevm error: OperationDoesntSupportBooleanInputs")); + } + } + } + + { // unary boolean inputs + let output_handle_a = next_handle(); + let async_computations = vec![ + AsyncComputation { + operation: FheOperation::FheNeg.into(), + output_handle: output_handle_a.clone(), + inputs: vec![ + AsyncComputationInput { + input: Some(Input::InputHandle(test_bool.handle.clone())), + }, + ], + }, + ]; + let mut input_request = tonic::Request::new(AsyncComputeRequest { + computations: async_computations, + }); + input_request.metadata_mut().append( + "authorization", + MetadataValue::from_str(&api_key_header).unwrap(), + ); + match client.async_compute(input_request).await { + Ok(_) => { + panic!("Expected failure") + } + Err(e) => { + eprintln!("error: {}", e); + assert!(e.to_string().contains("fhevm error: OperationDoesntSupportBooleanInputs")); + } + } + } + + Ok(()) +} \ No newline at end of file diff --git a/fhevm-engine/coprocessor/src/tests/mod.rs b/fhevm-engine/coprocessor/src/tests/mod.rs index e1db56bd..045abb6b 100644 --- a/fhevm-engine/coprocessor/src/tests/mod.rs +++ b/fhevm-engine/coprocessor/src/tests/mod.rs @@ -13,6 +13,7 @@ use utils::{default_api_key, wait_until_all_ciphertexts_computed}; mod operators; mod utils; mod inputs; +mod errors; #[tokio::test] async fn test_smoke() -> Result<(), Box> { diff --git a/fhevm-engine/coprocessor/src/types.rs b/fhevm-engine/coprocessor/src/types.rs index fe0fc40e..b5d828ec 100644 --- a/fhevm-engine/coprocessor/src/types.rs +++ b/fhevm-engine/coprocessor/src/types.rs @@ -56,13 +56,13 @@ impl std::fmt::Display for CoprocessorError { write!(f, "More than maximum input blobs uploaded, maximum allowed: {maximum_allowed}, uploaded: {input_count}") } Self::CompactInputCiphertextHasMoreCiphertextThanLimitAllows { input_blob_index, input_ciphertexts_in_blob, input_maximum_ciphertexts_allowed } => { - write!(f, "Input blob contains mismatching amount of ciphertexts, input blob index: {input_blob_index}, ciphertexts in blob: {input_ciphertexts_in_blob}, maximum ciphertexts in blob allowed: {input_maximum_ciphertexts_allowed}") + write!(f, "Input blob contains too many ciphertexts, input blob index: {input_blob_index}, ciphertexts in blob: {input_ciphertexts_in_blob}, maximum ciphertexts in blob allowed: {input_maximum_ciphertexts_allowed}") } Self::CiphertextHandleLongerThan64Bytes => { write!(f, "Found ciphertext handle longer than 64 bytes") } Self::CiphertextHandleMustBeAtLeast1Byte(handle) => { - write!(f, "Found ciphertext handle less than 4 bytes: {handle}") + write!(f, "Found ciphertext handle is empty: {handle}") } Self::UnexistingInputCiphertextsFound(handles) => { write!(f, "Ciphertexts not found: {:?}", handles) diff --git a/fhevm-engine/fhevm-engine-common/src/tfhe_ops.rs b/fhevm-engine/fhevm-engine-common/src/tfhe_ops.rs index f30411dc..3722a0b6 100644 --- a/fhevm-engine/fhevm-engine-common/src/tfhe_ops.rs +++ b/fhevm-engine/fhevm-engine-common/src/tfhe_ops.rs @@ -165,6 +165,7 @@ pub fn check_fhe_operand_types( assert_eq!(input_handles.len(), is_input_handle_scalar.len()); let fhe_op: SupportedFheOperations = fhe_operation.try_into()?; + let fhe_bool_type = 1; let scalar_operands = is_input_handle_scalar.iter().enumerate() .filter(|(_, is_scalar)| **is_scalar) @@ -227,6 +228,14 @@ pub fn check_fhe_operand_types( ); } + if input_types[0] == fhe_bool_type && !fhe_op.supports_bool_inputs() { + return Err(FhevmError::OperationDoesntSupportBooleanInputs { + fhe_operation, + fhe_operation_name: format!("{:?}", fhe_op), + operand_type: fhe_bool_type, + }); + } + // special case for div operation, rhs for scalar must be zero if is_scalar && fhe_op == SupportedFheOperations::FheDiv { let all_zeroes = input_handles[1].iter().all(|i| *i == 0u8); @@ -241,7 +250,7 @@ pub fn check_fhe_operand_types( } if fhe_op.is_comparison() { - return Ok(1); // fhe bool type + return Ok(fhe_bool_type); // fhe bool type } return Ok(input_types[0]); @@ -257,6 +266,15 @@ pub fn check_fhe_operand_types( }); } + let fhe_bool_type = 1; + if input_types[0] == fhe_bool_type && !fhe_op.supports_bool_inputs() { + return Err(FhevmError::OperationDoesntSupportBooleanInputs { + fhe_operation, + fhe_operation_name: format!("{:?}", fhe_op), + operand_type: fhe_bool_type, + }); + } + return Ok(input_types[0]); } FheOperationType::Other => { @@ -1023,6 +1041,9 @@ pub fn perform_fhe_operation( assert_eq!(input_operands.len(), 2); match (&input_operands[0], &input_operands[1]) { + (SupportedFheCiphertexts::FheBool(a), SupportedFheCiphertexts::FheBool(b)) => { + Ok(SupportedFheCiphertexts::FheBool(a.eq(b))) + } (SupportedFheCiphertexts::FheUint8(a), SupportedFheCiphertexts::FheUint8(b)) => { Ok(SupportedFheCiphertexts::FheBool(a.eq(b))) } @@ -1035,6 +1056,11 @@ pub fn perform_fhe_operation( (SupportedFheCiphertexts::FheUint64(a), SupportedFheCiphertexts::FheUint64(b)) => { Ok(SupportedFheCiphertexts::FheBool(a.eq(b))) } + (SupportedFheCiphertexts::FheBool(a), SupportedFheCiphertexts::Scalar(b)) => { + let (l, h) = b.to_low_high_u128(); + let non_zero = l > 0 || h > 0; + Ok(SupportedFheCiphertexts::FheBool(a.eq(non_zero))) + } (SupportedFheCiphertexts::FheUint8(a), SupportedFheCiphertexts::Scalar(b)) => { // TODO: figure out type to add correctly 256 bit operands from handles let (l, h) = b.to_low_high_u128(); @@ -1068,6 +1094,9 @@ pub fn perform_fhe_operation( assert_eq!(input_operands.len(), 2); match (&input_operands[0], &input_operands[1]) { + (SupportedFheCiphertexts::FheBool(a), SupportedFheCiphertexts::FheBool(b)) => { + Ok(SupportedFheCiphertexts::FheBool(a.ne(b))) + } (SupportedFheCiphertexts::FheUint8(a), SupportedFheCiphertexts::FheUint8(b)) => { Ok(SupportedFheCiphertexts::FheBool(a.ne(b))) } @@ -1080,6 +1109,11 @@ pub fn perform_fhe_operation( (SupportedFheCiphertexts::FheUint64(a), SupportedFheCiphertexts::FheUint64(b)) => { Ok(SupportedFheCiphertexts::FheBool(a.ne(b))) } + (SupportedFheCiphertexts::FheBool(a), SupportedFheCiphertexts::Scalar(b)) => { + let (l, h) = b.to_low_high_u128(); + let non_zero = l > 0 || h > 0; + Ok(SupportedFheCiphertexts::FheBool(a.ne(non_zero))) + } (SupportedFheCiphertexts::FheUint8(a), SupportedFheCiphertexts::Scalar(b)) => { // TODO: figure out type to add correctly 256 bit operands from handles let (l, h) = b.to_low_high_u128(); @@ -1293,6 +1327,7 @@ pub fn perform_fhe_operation( assert_eq!(input_operands.len(), 1); match &input_operands[0] { + SupportedFheCiphertexts::FheBool(a) => Ok(SupportedFheCiphertexts::FheBool(!a)), SupportedFheCiphertexts::FheUint8(a) => Ok(SupportedFheCiphertexts::FheUint8(!a)), SupportedFheCiphertexts::FheUint16(a) => Ok(SupportedFheCiphertexts::FheUint16(!a)), SupportedFheCiphertexts::FheUint32(a) => Ok(SupportedFheCiphertexts::FheUint32(!a)), diff --git a/fhevm-engine/fhevm-engine-common/src/types.rs b/fhevm-engine/fhevm-engine-common/src/types.rs index 60fdf71e..d07a0e22 100644 --- a/fhevm-engine/fhevm-engine-common/src/types.rs +++ b/fhevm-engine/fhevm-engine-common/src/types.rs @@ -41,6 +41,11 @@ pub enum FhevmError { expected_operands: usize, got_operands: usize, }, + OperationDoesntSupportBooleanInputs { + fhe_operation: i32, + fhe_operation_name: String, + operand_type: i16, + }, FheIfThenElseUnexpectedOperandTypes { fhe_operation: i32, fhe_operation_name: String, @@ -118,6 +123,13 @@ impl std::fmt::Display for FhevmError { } => { write!(f, "fhe operation number {fhe_operation} ({fhe_operation_name}) received unexpected operand count, expected: {expected_operands}, received: {got_operands}") } + Self::OperationDoesntSupportBooleanInputs { + fhe_operation, + fhe_operation_name, + operand_type, + } => { + write!(f, "fhe operation number {fhe_operation} ({fhe_operation_name}) does not support booleans as inputs, input type: {operand_type}") + } Self::FheOperationOnlySecondOperandCanBeScalar { scalar_input_index, only_allowed_scalar_input_index, @@ -292,6 +304,15 @@ impl SupportedFheOperations { _ => false, } } + + pub fn supports_bool_inputs(&self) -> bool { + match self { + SupportedFheOperations::FheEq + | SupportedFheOperations::FheNe + | SupportedFheOperations::FheNot => true, + _ => false, + } + } } impl TryFrom for SupportedFheOperations {