#pragma once #include #include #include #include #include #include #include #include namespace homcert::bfv { constexpr std::size_t VECSIZE = 8192; constexpr std::size_t PLAINMOD = 16760833; // 8192*2*1023+1 and prime template struct smallest_uint; template struct smallest_uint> { using type = std::uint8_t; }; template struct smallest_uint> { using type = std::uint16_t; }; template struct smallest_uint> { using type = std::uint32_t; }; template struct smallest_uint> { using type = std::uint64_t; }; struct plaintext { using coefficient = typename smallest_uint::type; std::array coefficients; // Arithmetic... }; using ciphertext = int; // Exception for BFV errors in context class bfv_exception : public std::exception { public: enum class REASON { CTX_INVALID, CTX_NOT_IMPLEMENTED, CTX_NO_PRIVATE, CTX_NO_PUBLIC, CTX_ARITHMETIC, CTX_MEMORY, CTX_INVALID_ARGUMENT, CTX_COMPONENT, CTX_INTERNAL }; private: REASON m_reason; std::string m_message; public: bfv_exception(REASON reason, std::string message); REASON reason() const noexcept; const char* what() const noexcept override; }; // Inheriting from context class allows pluggability of implementations (e.g. SEAL vs. GPU) struct context { static constexpr int PUBLIC_COMPONENT = 1; static constexpr int PRIVATE_COMPONENT = 2; context(const context& other) = delete; context(context&& other) = delete; context& operator=(const context& other) = delete; context& operator=(context&& other) = delete; context() = default; virtual ~context() = default; virtual void new_components() = 0; // context default constructs empty virtual void has_components(int& components) const = 0; virtual void clone_components(int components, std::shared_ptr& ptr) const = 0; // virtual void dump_components(int components, void* buf, std::size_t& n) const = 0; // virtual void load_components(int components, const void* buf, std::size_t n) = 0; virtual void allocate(ciphertext& ct) = 0; virtual void free(ciphertext ct) = 0; virtual void serialize(ciphertext ct, void* buf, std::size_t& n) const = 0; virtual void deserialize(ciphertext ct, const void* buf, std::size_t n) const = 0; virtual void encrypt(const plaintext& pt, ciphertext ct) const = 0; virtual void decrypt(ciphertext ct, plaintext& pt) const = 0; // private virtual void add_cipher_plain(ciphertext a, const plaintext& b, ciphertext res) const = 0; virtual void add_cipher_cipher(ciphertext a, ciphertext b, ciphertext res) const = 0; virtual void sub_cipher_plain(ciphertext a, const plaintext& b, ciphertext res) const = 0; virtual void sub_cipher_cipher(ciphertext a, ciphertext b, ciphertext res) const = 0; virtual void mul_cipher_plain(ciphertext a, const plaintext& b, ciphertext res) const = 0; virtual void mul_cipher_cipher(ciphertext a, ciphertext b, ciphertext res) const = 0; virtual void rot_cipher_rows(ciphertext ct, int r, ciphertext res) const = 0; virtual void swap_cipher_rows(ciphertext ct, ciphertext res) const = 0; virtual void noise_budget(ciphertext ct, std::size_t& budget) const = 0; // private }; /* NOTE -> Implementations may defer operations or not wait for them to finish For example, GPU implementations may dispatch multiplications when they arive Additionally, a graph of running operations is maintained to handle data dependencies Only when data is required to actually be present (e.g. decrypt), does the implementation wait */ // BFV implementation using Microsoft SEAL struct seal_context : public context { seal_context(); ~seal_context(); void new_components() override; void has_components(int& components) const override; void clone_components(int components, std::shared_ptr& ptr) const override; void allocate(ciphertext& ct) override; void free(ciphertext ct) override; void serialize(ciphertext ct, void* buf, std::size_t& n) const override; void deserialize(ciphertext ct, const void* buf, std::size_t n) const override; void encrypt(const plaintext& pt, ciphertext ct) const override; void decrypt(ciphertext ct, plaintext& pt) const override; void add_cipher_plain(ciphertext a, const plaintext& b, ciphertext res) const override; void add_cipher_cipher(ciphertext a, ciphertext b, ciphertext res) const override; void sub_cipher_plain(ciphertext a, const plaintext& b, ciphertext res) const override; void sub_cipher_cipher(ciphertext a, ciphertext b, ciphertext res) const override; void mul_cipher_plain(ciphertext a, const plaintext& b, ciphertext res) const override; void mul_cipher_cipher(ciphertext a, ciphertext b, ciphertext res) const override; void rot_cipher_rows(ciphertext ct, int r, ciphertext res) const override; void swap_cipher_rows(ciphertext ct, ciphertext res) const override; void noise_budget(ciphertext ct, std::size_t& budget) const override; private: int m_id; }; /* activate_context(std::shared_ptr ctx) -> thread local pointer is set Raw ciphertext and plaintext classes always have the full 8192 coefficients (defined in context as static constexpr) bfv::vector<...> -> can be plaintext or ciphertext -> can be base (owns plain-/ciphertext) or component (view to part of base) -> can be local or remote -> can be a single vector or multiple vectors/components (variadic) -> arithmetic with component masks it out -> arithmetic with base does operation on all components -> tracks multiplicative depth -> warning/error if multiplicative depth exceeds limit -> use bootstrap member function to handle the warnings/errors -> callbacks to reach peer in context -> bootstrap_client (unchecked, just raw bootstrap, checks happen at an upper layer using other callbacks) -> bootstrap_server_await (waits for client to make request) -> bootstrap_server_serve (called immediately after request received with value to be returned) -> automatically does secure reveal when cipher is transformed to plain -> queues operations until used (cast to plaintext, communication with peer) Programs are defined TWICE -> local stuff is executed -> remote stuff is hosted (e.g. bootstrapping server) -> defined once from each side (differ e.g. in the plaintext inputs etc.) -> program base class may be used to handle context setting */ }