aboutsummaryrefslogtreecommitdiffhomepage
path: root/tde2e
diff options
context:
space:
mode:
authorArseny Smirnov <arseny30@gmail.com>2024-10-04 01:19:51 +0200
committerArseny Smirnov <arseny30@gmail.com>2025-04-10 17:18:46 +0300
commit6a46f51bdf5e5052ddae26b61cdf9d4864238442 (patch)
treebd7d1af6a2b68689cb97fc9391eeaee8851dada6 /tde2e
parent6571af5d49bf9b05361187599b156e93face92df (diff)
Add tde2e library
Diffstat (limited to 'tde2e')
-rw-r--r--tde2e/CMakeLists.txt86
-rw-r--r--tde2e/td/e2e/BitString.cpp355
-rw-r--r--tde2e/td/e2e/BitString.h62
-rw-r--r--tde2e/td/e2e/Blockchain.cpp821
-rw-r--r--tde2e/td/e2e/Blockchain.h351
-rw-r--r--tde2e/td/e2e/Blockchain.md194
-rw-r--r--tde2e/td/e2e/Call.cpp792
-rw-r--r--tde2e/td/e2e/Call.h235
-rw-r--r--tde2e/td/e2e/CheckSharedSecret.cpp75
-rw-r--r--tde2e/td/e2e/CheckSharedSecret.h32
-rw-r--r--tde2e/td/e2e/Container.h256
-rw-r--r--tde2e/td/e2e/DecryptedKey.cpp40
-rw-r--r--tde2e/td/e2e/DecryptedKey.h52
-rw-r--r--tde2e/td/e2e/EncryptedKey.cpp38
-rw-r--r--tde2e/td/e2e/EncryptedKey.h29
-rw-r--r--tde2e/td/e2e/EncryptedStorage.cpp369
-rw-r--r--tde2e/td/e2e/EncryptedStorage.h410
-rw-r--r--tde2e/td/e2e/Encryption.md183
-rw-r--r--tde2e/td/e2e/Keys.cpp208
-rw-r--r--tde2e/td/e2e/Keys.h104
-rw-r--r--tde2e/td/e2e/MessageEncryption.cpp196
-rw-r--r--tde2e/td/e2e/MessageEncryption.h46
-rw-r--r--tde2e/td/e2e/Mnemonic.cpp264
-rw-r--r--tde2e/td/e2e/Mnemonic.h59
-rw-r--r--tde2e/td/e2e/QRHandshake.cpp227
-rw-r--r--tde2e/td/e2e/QRHandshake.h91
-rw-r--r--tde2e/td/e2e/TestBlockchain.cpp771
-rw-r--r--tde2e/td/e2e/TestBlockchain.h233
-rw-r--r--tde2e/td/e2e/Trie.cpp506
-rw-r--r--tde2e/td/e2e/Trie.h102
-rw-r--r--tde2e/td/e2e/bip39.cpp2063
-rw-r--r--tde2e/td/e2e/bip39.h15
-rw-r--r--tde2e/td/e2e/e2e_api.cpp857
-rw-r--r--tde2e/td/e2e/e2e_api.h336
-rw-r--r--tde2e/td/e2e/e2e_errors.h102
-rw-r--r--tde2e/td/e2e/encryption_test.py314
-rw-r--r--tde2e/td/e2e/utils.h151
-rw-r--r--tde2e/test/EncryptionTestVectors.h88
-rw-r--r--tde2e/test/blockchain.cpp251
-rw-r--r--tde2e/test/e2e.cpp914
-rw-r--r--tde2e/test/encryption.cpp65
41 files changed, 12343 insertions, 0 deletions
diff --git a/tde2e/CMakeLists.txt b/tde2e/CMakeLists.txt
new file mode 100644
index 000000000..3e6e91a59
--- /dev/null
+++ b/tde2e/CMakeLists.txt
@@ -0,0 +1,86 @@
+if ((CMAKE_MAJOR_VERSION LESS 3) OR (CMAKE_VERSION VERSION_LESS "3.10"))
+ message(FATAL_ERROR "CMake >= 3.10 is required")
+endif()
+
+option(TDE2E_ENABLE_INSTALL "Enable installation of the library." ON)
+
+if (NOT DEFINED CMAKE_INSTALL_LIBDIR)
+ set(CMAKE_INSTALL_LIBDIR "lib")
+endif()
+
+set_source_files_properties(${TL_E2E_AUTO_SOURCE} PROPERTIES GENERATED TRUE)
+
+set(TDE2E_SOURCE
+ td/e2e/bip39.cpp
+ td/e2e/BitString.cpp
+ td/e2e/Blockchain.cpp
+ td/e2e/Call.cpp
+ td/e2e/CheckSharedSecret.cpp
+ td/e2e/DecryptedKey.cpp
+ td/e2e/e2e_api.cpp
+ td/e2e/EncryptedKey.cpp
+ td/e2e/EncryptedStorage.cpp
+ td/e2e/Keys.cpp
+ td/e2e/MessageEncryption.cpp
+ td/e2e/Mnemonic.cpp
+ td/e2e/QRHandshake.cpp
+ td/e2e/Trie.cpp
+
+ td/e2e/bip39.h
+ td/e2e/BitString.h
+ td/e2e/Blockchain.h
+ td/e2e/Call.h
+ td/e2e/CheckSharedSecret.h
+ td/e2e/Container.h
+ td/e2e/DecryptedKey.h
+ td/e2e/e2e_api.h
+ td/e2e/e2e_errors.h
+ td/e2e/EncryptedKey.h
+ td/e2e/EncryptedStorage.h
+ td/e2e/Keys.h
+ td/e2e/MessageEncryption.h
+ td/e2e/Mnemonic.h
+ td/e2e/QRHandshake.h
+ td/e2e/Trie.h
+ td/e2e/utils.h
+
+ ${TL_E2E_AUTO_SOURCE}
+
+ ../td/tl/TlObject.h
+ ../td/tl/tl_object_parse.h
+ ../td/tl/tl_object_store.h
+)
+
+set(TDE2E_TEST_SOURCE
+ ${CMAKE_CURRENT_SOURCE_DIR}/td/e2e/TestBlockchain.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/td/e2e/TestBlockchain.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/test/blockchain.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/test/e2e.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/test/encryption.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/test/EncryptionTestVectors.h
+)
+
+set(TDE2E_TEST_SOURCE "${TDE2E_TEST_SOURCE}" PARENT_SCOPE)
+
+add_library(tde2e STATIC ${TDE2E_SOURCE})
+if (NOT CMAKE_CROSSCOMPILING)
+ add_dependencies(tde2e tl_generate_common)
+endif()
+target_link_libraries(tde2e PUBLIC tdutils PRIVATE ${OPENSSL_CRYPTO_LIBRARY} ${CMAKE_DL_LIBS} ${ZLIB_LIBRARIES})
+target_include_directories(tde2e SYSTEM PRIVATE $<BUILD_INTERFACE:${OPENSSL_INCLUDE_DIR}>)
+target_include_directories(tde2e PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>/..)
+target_include_directories(tde2e PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>)
+target_include_directories(tde2e PUBLIC $<BUILD_INTERFACE:${TL_TD_AUTO_INCLUDE_DIR}>)
+
+add_executable(test-e2e EXCLUDE_FROM_ALL
+ ${TDE2E_TEST_SOURCE}
+ ../test/main.cpp)
+target_link_libraries(test-e2e PRIVATE tde2e)
+
+
+if (TDE2E_ENABLE_INSTALL)
+ install(TARGETS tde2e EXPORT TdStaticTargets
+ LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
+ ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
+ )
+endif()
diff --git a/tde2e/td/e2e/BitString.cpp b/tde2e/td/e2e/BitString.cpp
new file mode 100644
index 000000000..4ad1a8c77
--- /dev/null
+++ b/tde2e/td/e2e/BitString.cpp
@@ -0,0 +1,355 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/e2e/BitString.h"
+
+#include "td/utils/bits.h"
+#include "td/utils/common.h"
+#include "td/utils/logging.h"
+#include "td/utils/misc.h"
+#ifndef TG_ENGINE
+#include "td/utils/ThreadSafeCounter.h"
+#endif
+#include "td/utils/tl_helpers.h"
+#include "td/utils/tl_parsers.h"
+#include "td/utils/tl_storers.h"
+
+#include <algorithm>
+#include <cstring>
+
+namespace tde2e_core {
+
+namespace {
+td::uint8 begin_mask(size_t start_bit) {
+ return 0xFF >> start_bit;
+}
+td::uint8 end_mask(size_t end_bit) {
+ return static_cast<td::uint8>(0xFF << (8 - end_bit));
+}
+td::uint8 create_mask(size_t start_bit, size_t end_bit) {
+ return begin_mask(start_bit) & end_mask(end_bit);
+}
+
+size_t count_common_bits(td::uint8 byte1, td::uint8 byte2, size_t start_bit, size_t end_bit) {
+ return td::count_leading_zeroes32(((byte1 ^ byte2) & begin_mask(start_bit)) >> (8 - end_bit)) +
+ (end_bit - start_bit) - 32;
+}
+#ifndef TG_ENGINE
+td::NamedThreadSafeCounter::CounterRef &get_bit_string_counter() {
+ static auto counter = td::NamedThreadSafeCounter::get_default().get_counter("BitString");
+ return counter;
+}
+#endif
+} // namespace
+
+td::int64 BitString::get_counter_value() {
+ return 0;
+}
+
+BitString::BitString(size_t bits) : BitString(nullptr, 0, bits) {
+}
+
+BitString::BitString(std::shared_ptr<char> ptr, size_t offset, size_t size) {
+ size_t begin = offset;
+ size_t end = offset + size;
+
+ size_t begin_byte = (begin + 7) / 8;
+ size_t end_byte = end / 8;
+
+ bits_size_ = size;
+ bytes_size_ = static_cast<td::int32>(end_byte) - static_cast<td::int32>(begin_byte);
+ begin_bit_ = begin % 8;
+ end_bit_ = end % 8;
+ CHECK(bytes_size_ != -1 || (begin_bit_ && end_bit_));
+ if (!ptr) {
+ auto full_size = bytes_size_ + (begin_bit_ != 0) + (end_bit_ != 0);
+ ptr = std::shared_ptr<char>(new char[full_size], std::default_delete<char[]>());
+ td::MutableSlice(ptr.get(), full_size).fill_zero();
+#ifndef TG_ENGINE
+ get_bit_string_counter().add(+1);
+#endif
+ data_ = std::shared_ptr<char>(ptr, ptr.get() + (begin_bit_ != 0));
+ } else {
+ data_ = std::shared_ptr<char>(ptr, ptr.get() + begin_byte);
+ }
+}
+
+BitString::BitString(td::Slice key_data) : BitString(nullptr, 0, key_data.size() * 8) {
+ td::MutableSlice(data_.get(), key_data.size()).copy_from(key_data);
+}
+
+BitString::~BitString() {
+ if (data_.use_count() == 1) {
+#ifndef TG_ENGINE
+ get_bit_string_counter().add(-1);
+#endif
+ }
+}
+
+BitString &BitString::operator=(const BitString &other) {
+ if (&other == this) {
+ return *this;
+ }
+ LOG_CHECK(!data_) << static_cast<void *>(data_.get());
+ data_ = other.data_;
+ bits_size_ = other.bits_size_;
+ bytes_size_ = other.bytes_size_;
+ begin_bit_ = other.begin_bit_;
+ end_bit_ = other.end_bit_;
+ return *this;
+}
+
+BitString &BitString::operator=(BitString &&other) noexcept {
+ LOG_CHECK(!data_) << static_cast<void *>(data_.get());
+ data_ = std::move(other.data_);
+ bits_size_ = other.bits_size_;
+ bytes_size_ = other.bytes_size_;
+ begin_bit_ = other.begin_bit_;
+ end_bit_ = other.end_bit_;
+ return *this;
+}
+
+size_t BitString::bit_length() const {
+ return bits_size_;
+}
+
+td::uint8 BitString::get_bit(size_t pos) const {
+ CHECK(pos < bit_length());
+ size_t absolute_bit_pos = pos + begin_bit_;
+ size_t byte_index = absolute_bit_pos / 8 - (begin_bit_ != 0);
+ size_t bit_index = 7 - (absolute_bit_pos % 8); // Big-endian bit order
+ return (data_.get()[byte_index] >> bit_index) & 1;
+}
+
+bool BitString::operator==(const BitString &other) const {
+ if (bit_length() != other.bit_length()) {
+ return false;
+ }
+ if (bit_length() == 0) {
+ return true;
+ }
+ CHECK(begin_bit_ == other.begin_bit_);
+ CHECK(bytes_size_ == other.bytes_size_);
+ CHECK(end_bit_ == other.end_bit_);
+
+ auto ptr1 = data_.get();
+ auto ptr2 = other.data_.get();
+ if (bytes_size_ == -1) {
+ td::uint8 mask = create_mask(begin_bit_, end_bit_);
+ return (ptr1[-1] & mask) == (ptr2[-1] & mask);
+ }
+
+ if (begin_bit_ != 0) {
+ td::uint8 first_byte_mask = begin_mask(begin_bit_);
+ if ((ptr1[-1] & first_byte_mask) != (ptr2[-1] & first_byte_mask)) {
+ return false;
+ }
+ }
+
+ if (end_bit_ != 0) {
+ td::uint8 last_byte_mask = end_mask(end_bit_);
+ if ((ptr1[bytes_size_] & last_byte_mask) != (ptr2[bytes_size_] & last_byte_mask)) {
+ return false;
+ }
+ }
+
+ return std::memcmp(ptr1, ptr2, bytes_size_) == 0;
+}
+
+size_t BitString::common_prefix_length(const BitString &other) const {
+ CHECK(begin_bit_ == other.begin_bit_);
+ //CHECK(bytes_size_ == other.bytes_size_);
+ //CHECK(end_bit_ == other.end_bit_);
+
+ td::uint8 begin_bit;
+ td::uint8 end_bit;
+ td::int32 bytes_size;
+ auto min_length = std::min(bit_length(), other.bit_length());
+ if (bit_length() < other.bit_length()) {
+ begin_bit = begin_bit_;
+ end_bit = end_bit_;
+ bytes_size = bytes_size_;
+ } else {
+ begin_bit = other.begin_bit_;
+ end_bit = other.end_bit_;
+ bytes_size = other.bytes_size_;
+ }
+
+ auto ptr1 = data_.get();
+ auto ptr2 = other.data_.get();
+
+ if (bytes_size == -1) {
+ auto res = count_common_bits(ptr1[-1], ptr2[-1], begin_bit, end_bit);
+ CHECK(res <= min_length);
+ return res;
+ }
+
+ size_t res = 0;
+
+ if (begin_bit != 0) {
+ td::uint8 first_byte_mask = begin_mask(begin_bit);
+ td::uint8 byte1 = ptr1[-1] & first_byte_mask;
+ td::uint8 byte2 = ptr2[-1] & first_byte_mask;
+ if (byte1 != byte2) {
+ res += count_common_bits(byte1, byte2, begin_bit, 8);
+ CHECK(res <= min_length);
+ return res;
+ }
+ res += 8 - begin_bit;
+ }
+
+ size_t first_diff = std::mismatch(ptr1, ptr1 + bytes_size, ptr2).first - ptr1;
+ res += first_diff * 8;
+ if (td::narrow_cast<int>(first_diff) != bytes_size) {
+ res += count_common_bits(ptr1[first_diff], ptr2[first_diff], 0, 8);
+ CHECK(res <= min_length);
+ return res;
+ }
+
+ if (end_bit != 0) {
+ res += count_common_bits(ptr1[bytes_size], ptr2[bytes_size], 0, end_bit);
+ CHECK(res <= min_length);
+ return res;
+ }
+ CHECK(res <= min_length);
+ return res;
+}
+
+BitString BitString::substr(size_t pos, size_t length) const {
+ auto size = bit_length();
+ CHECK(pos <= size);
+ size_t new_length = std::min(length, size - pos);
+ return BitString(std::shared_ptr<char>(data_, data_.get() - (begin_bit_ != 0)), begin_bit_ + pos, new_length);
+}
+
+template <class StorerT>
+void store(const BitString &bs, StorerT &storer) {
+ using td::store;
+ auto ptr = bs.data_.get();
+
+ store(static_cast<td::uint32>((static_cast<td::uint16>(bs.begin_bit_) << 16) |
+ static_cast<td::uint16>(bs.begin_bit_ + bs.bit_length())),
+ storer);
+
+ size_t n = 0;
+ if (bs.bytes_size_ == -1) {
+ td::uint8 mask = create_mask(bs.begin_bit_, bs.end_bit_);
+ storer.store_binary(static_cast<td::uint8>(ptr[-1] & mask));
+ n = 1;
+ } else {
+ if (bs.begin_bit_ != 0) {
+ td::uint8 first_byte_mask = begin_mask(bs.begin_bit_);
+ storer.store_binary(static_cast<td::uint8>(ptr[-1] & first_byte_mask));
+ n++;
+ }
+
+ storer.store_slice(td::Slice(ptr, bs.bytes_size_));
+ n += bs.bytes_size_;
+
+ if (bs.end_bit_ != 0) {
+ auto last_byte_mask = end_mask(bs.end_bit_);
+ storer.store_binary(static_cast<td::uint8>(ptr[bs.bytes_size_] & last_byte_mask));
+ n++;
+ }
+ }
+ while (n % 4 != 0) {
+ storer.store_binary(static_cast<td::uint8>(0));
+ n++;
+ }
+}
+
+template <class ParserT>
+BitString fetch_bit_string(ParserT &parser) {
+ BitString base_bs;
+ return fetch_bit_string(parser, base_bs);
+}
+
+template <class ParserT>
+BitString fetch_bit_string(ParserT &parser, BitString &base_bs) {
+ using td::parse;
+ td::uint32 begin_end;
+ parse(begin_end, parser);
+
+ size_t begin = begin_end >> 16;
+ size_t end = begin_end & 0xFFFF;
+ auto bs = base_bs.data_ ? base_bs.substr(0, end - begin) : BitString(nullptr, begin, end - begin);
+
+ auto ptr = bs.data_.get();
+
+ size_t n = 0;
+ td::uint8 byte;
+ if (bs.bytes_size_ == -1) {
+ td::uint8 mask = create_mask(bs.begin_bit_, bs.end_bit_);
+ byte = parser.template fetch_binary<td::uint8>();
+ ptr[-1] |= byte & mask;
+ n = 1;
+ } else {
+ if (bs.begin_bit_ != 0) {
+ byte = parser.template fetch_binary<td::uint8>();
+ td::uint8 first_byte_mask = begin_mask(bs.begin_bit_);
+ ptr[-1] |= byte & first_byte_mask;
+ n++;
+ }
+
+ td::MutableSlice(ptr, bs.bytes_size_).copy_from(parser.template fetch_string_raw<td::Slice>(bs.bytes_size_));
+ n += bs.bytes_size_;
+
+ if (bs.end_bit_ != 0) {
+ byte = parser.template fetch_binary<td::uint8>();
+ auto last_byte_mask = end_mask(bs.end_bit_);
+ ptr[bs.bytes_size_] |= byte & last_byte_mask;
+ n++;
+ }
+ }
+ while (n % 4 != 0) {
+ byte = parser.template fetch_binary<td::uint8>();
+ n++;
+ }
+ return bs;
+}
+template void store<td::TlStorerUnsafe>(const BitString &bs, td::TlStorerUnsafe &storer);
+template void store<td::TlStorerCalcLength>(const BitString &bs, td::TlStorerCalcLength &storer);
+
+template BitString fetch_bit_string<td::TlParser>(td::TlParser &fetch_bit_stringr);
+template BitString fetch_bit_string<td::TlParser>(td::TlParser &fetch_bit_stringr, BitString &base_bs);
+
+td::Result<std::string> BitString::serialize_for_network(const BitString &bs) {
+ td::TlStorerCalcLength calc_length;
+ store(bs, calc_length);
+ std::string buf(calc_length.get_length(), 0);
+ td::TlStorerUnsafe storer(td::MutableSlice(buf).ubegin());
+ store(bs, storer);
+ return buf;
+}
+td::Result<BitString> BitString::fetch_from_network(td::Slice data) {
+ td::TlParser parser(data);
+ auto res = fetch_bit_string(parser);
+ parser.fetch_end();
+ TRY_STATUS(parser.get_status());
+ return res;
+}
+
+std::ostream &operator<<(std::ostream &os, const BitString &bits) {
+ os << static_cast<td::uint32>(bits.begin_bit_) << ' ' << bits.bytes_size_ << ' '
+ << static_cast<td::uint32>(bits.end_bit_) << ' ';
+ for (size_t i = 0; i < bits.bit_length(); ++i) {
+ os << static_cast<int>(bits.get_bit(i));
+ }
+ os << ' ' << bits.data_.get();
+ return os;
+}
+
+td::StringBuilder &operator<<(td::StringBuilder &string_builder, const BitString &bits) {
+ string_builder << static_cast<td::uint32>(bits.begin_bit_) << ' ' << bits.bytes_size_ << ' '
+ << static_cast<td::uint32>(bits.end_bit_) << ' ';
+ for (size_t i = 0; i < bits.bit_length(); ++i) {
+ string_builder << static_cast<int>(bits.get_bit(i));
+ }
+ string_builder << ' ' << bits.data_.get();
+ return string_builder;
+}
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/BitString.h b/tde2e/td/e2e/BitString.h
new file mode 100644
index 000000000..b58823d11
--- /dev/null
+++ b/tde2e/td/e2e/BitString.h
@@ -0,0 +1,62 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include "td/e2e/utils.h"
+
+#include "td/utils/common.h"
+#include "td/utils/Slice.h"
+#include "td/utils/Status.h"
+#include "td/utils/StringBuilder.h"
+
+#include <memory>
+#include <ostream>
+
+namespace tde2e_core {
+
+class BitString {
+ public:
+ static td::int64 get_counter_value();
+ BitString() = default;
+ explicit BitString(size_t bits);
+ BitString(std::shared_ptr<char> ptr, size_t offset, size_t size);
+ explicit BitString(td::Slice key_data);
+ BitString(const BitString &) = default;
+ BitString &operator=(const BitString &other);
+ BitString(BitString &&) noexcept = default;
+ BitString &operator=(BitString &&other) noexcept;
+ ~BitString();
+
+ size_t bit_length() const;
+ td::uint8 get_bit(size_t pos) const;
+
+ bool operator==(const BitString &other) const;
+ size_t common_prefix_length(const BitString &other) const;
+ BitString substr(size_t pos, size_t length = SIZE_MAX) const;
+
+ friend std::ostream &operator<<(std::ostream &os, const BitString &bits);
+ friend td::StringBuilder &operator<<(td::StringBuilder &string_builder, const BitString &bits);
+
+ template <class StorerT>
+ friend void store(const BitString &bs, StorerT &storer);
+ template <class ParserT>
+ friend BitString fetch_bit_string(ParserT &parser);
+ template <class ParserT>
+ friend BitString fetch_bit_string(ParserT &parser, BitString &base_bs);
+
+ static td::Result<std::string> serialize_for_network(const BitString &bs);
+ static td::Result<BitString> fetch_from_network(td::Slice data);
+
+ // TODO: make following fields private
+ std::shared_ptr<char> data_;
+ size_t bits_size_{};
+ td::int32 bytes_size_{};
+ td::uint8 begin_bit_{};
+ td::uint8 end_bit_{};
+};
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/Blockchain.cpp b/tde2e/td/e2e/Blockchain.cpp
new file mode 100644
index 000000000..51cbf758a
--- /dev/null
+++ b/tde2e/td/e2e/Blockchain.cpp
@@ -0,0 +1,821 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/e2e/Blockchain.h"
+
+#include "td/e2e/Keys.h"
+
+#include "td/telegram/e2e_api.hpp"
+
+#include "td/utils/algorithm.h"
+#include "td/utils/as.h"
+#include "td/utils/common.h"
+#include "td/utils/crypto.h"
+#include "td/utils/format.h"
+#include "td/utils/misc.h"
+#include "td/utils/overloaded.h"
+#include "td/utils/SliceBuilder.h"
+#include "td/utils/tl_parsers.h"
+
+#include <algorithm>
+#include <limits>
+#include <map>
+#include <set>
+#include <tuple>
+#include <utility>
+
+namespace tde2e_core {
+
+GroupParticipant GroupParticipant::from_tl(const td::e2e_api::e2e_chain_groupParticipant &participant) {
+ return GroupParticipant{participant.user_id_, participant.flags_, PublicKey::from_u256(participant.public_key_),
+ participant.version_};
+}
+
+e2e::object_ptr<e2e::e2e_chain_groupParticipant> GroupParticipant::to_tl() const {
+ return e2e::make_object<e2e::e2e_chain_groupParticipant>(user_id, public_key.to_u256(), flags, add_users(),
+ remove_users(), version);
+}
+
+td::int32 GroupState::version() const {
+ if (participants.empty()) {
+ return 0;
+ }
+ td::int32 version = participants.front().version;
+ for (auto &participant : participants) {
+ version = std::min(version, participant.version);
+ }
+ return std::clamp(version, 0, 255);
+}
+
+td::Result<GroupParticipant> GroupState::get_participant(td::int64 user_id) const {
+ for (const auto &participant : participants) {
+ if (participant.user_id == user_id) {
+ return participant;
+ }
+ }
+ return td::Status::Error("Participant not found");
+}
+
+td::Result<GroupParticipant> GroupState::get_participant(const PublicKey &public_key) const {
+ for (const auto &participant : participants) {
+ if (participant.public_key == public_key) {
+ return participant;
+ }
+ }
+ return td::Status::Error("Participant not found");
+}
+
+Permissions GroupState::get_permissions(const PublicKey &public_key, td::int32 limit_permissions) const {
+ limit_permissions &= GroupParticipantFlags::AllPermissions;
+ auto r_participant = get_participant(public_key);
+ if (r_participant.is_ok()) {
+ return Permissions{(r_participant.ok().flags & limit_permissions) | GroupParticipantFlags::IsParticipant};
+ }
+ return Permissions{(external_permissions & limit_permissions)};
+}
+
+GroupStateRef GroupState::from_tl(const td::e2e_api::e2e_chain_groupState &state) {
+ auto participant_from_tl = [](const td::e2e_api::object_ptr<td::e2e_api::e2e_chain_groupParticipant> &participant) {
+ return GroupParticipant::from_tl(*participant);
+ };
+ td::optional<td::int32> external_permissions{};
+ return std::make_shared<GroupState>(
+ GroupState{td::transform(state.participants_, participant_from_tl), state.external_permissions_});
+}
+
+e2e::object_ptr<e2e::e2e_chain_groupState> GroupState::to_tl() const {
+ return e2e::make_object<e2e::e2e_chain_groupState>(
+ td::transform(participants, [](const GroupParticipant &participant) { return participant.to_tl(); }),
+ external_permissions);
+}
+
+GroupStateRef GroupState::empty_state() {
+ static GroupStateRef state = std::make_shared<GroupState>();
+ return state;
+}
+
+GroupSharedKeyRef GroupSharedKey::from_tl(const td::e2e_api::e2e_chain_sharedKey &shared_key) {
+ return std::make_shared<GroupSharedKey>(GroupSharedKey{PublicKey::from_u256(shared_key.ek_),
+ shared_key.encrypted_shared_key_, shared_key.dest_user_id_,
+ shared_key.dest_header_});
+}
+
+e2e::object_ptr<e2e::e2e_chain_sharedKey> GroupSharedKey::to_tl() const {
+ return e2e::make_object<e2e::e2e_chain_sharedKey>(ek.to_u256(), encrypted_shared_key,
+ std::vector<td::int64>(dest_user_id), std::vector(dest_header));
+}
+
+GroupSharedKeyRef GroupSharedKey::empty_shared_key() {
+ static GroupSharedKeyRef shared_key = std::make_shared<GroupSharedKey>();
+ return shared_key;
+}
+
+ChangeSetValue ChangeSetValue::from_tl(const td::e2e_api::e2e_chain_changeSetValue &change) {
+ return ChangeSetValue{change.key_, change.value_};
+}
+
+e2e::object_ptr<e2e::e2e_chain_changeSetValue> ChangeSetValue::to_tl() const {
+ return e2e::make_object<e2e::e2e_chain_changeSetValue>(key, value);
+}
+
+ChangeSetGroupState ChangeSetGroupState::from_tl(const td::e2e_api::e2e_chain_changeSetGroupState &change) {
+ return ChangeSetGroupState{GroupState::from_tl(*change.group_state_)};
+}
+
+e2e::object_ptr<e2e::e2e_chain_changeSetGroupState> ChangeSetGroupState::to_tl() const {
+ return e2e::make_object<e2e::e2e_chain_changeSetGroupState>(group_state->to_tl());
+}
+
+ChangeSetSharedKey ChangeSetSharedKey::from_tl(const td::e2e_api::e2e_chain_changeSetSharedKey &change) {
+ return ChangeSetSharedKey{GroupSharedKey::from_tl(*change.shared_key_)};
+}
+
+e2e::object_ptr<e2e::e2e_chain_changeSetSharedKey> ChangeSetSharedKey::to_tl() const {
+ return e2e::make_object<e2e::e2e_chain_changeSetSharedKey>(shared_key->to_tl());
+}
+
+Change Change::from_tl(const td::e2e_api::e2e_chain_Change &change) {
+ Change res;
+ downcast_call(
+ const_cast<td::e2e_api::e2e_chain_Change &>(change),
+ td::overloaded(
+ [&](td::e2e_api::e2e_chain_changeNoop &change_t) { res.value = ChangeNoop::from_tl(change_t); },
+ [&](td::e2e_api::e2e_chain_changeSetValue &change_t) { res.value = ChangeSetValue::from_tl(change_t); },
+ [&](td::e2e_api::e2e_chain_changeSetGroupState &change_t) {
+ res.value = ChangeSetGroupState::from_tl(change_t);
+ },
+ [&](td::e2e_api::e2e_chain_changeSetSharedKey &change_t) {
+ res.value = ChangeSetSharedKey::from_tl(change_t);
+ }));
+ return res;
+}
+
+e2e::object_ptr<e2e::e2e_chain_Change> Change::to_tl() const {
+ return std::visit(
+ td::overloaded(
+ [](const ChangeNoop &change) -> td::e2e_api::object_ptr<e2e::e2e_chain_Change> { return change.to_tl(); },
+ [](const ChangeSetValue &change) -> td::e2e_api::object_ptr<e2e::e2e_chain_Change> { return change.to_tl(); },
+ [](const ChangeSetGroupState &change) -> td::e2e_api::object_ptr<e2e::e2e_chain_Change> {
+ return change.to_tl();
+ },
+ [](const ChangeSetSharedKey &change) -> td::e2e_api::object_ptr<e2e::e2e_chain_Change> {
+ return change.to_tl();
+ }),
+ value);
+}
+
+td::UInt256 Block::calc_hash() const {
+ if (height_ == -1) {
+ return {};
+ }
+ auto serialized_block = serialize_boxed(*to_tl());
+ td::UInt256 hash;
+ td::sha256(serialized_block, hash.as_mutable_slice());
+ return hash;
+}
+
+Block Block::from_tl(const e2e::e2e_chain_block &block) {
+ Block result;
+ result.state_proof_ = StateProof::from_tl(*block.state_proof_);
+ if (block.flags_ & 1) {
+ result.o_signature_public_key_ = PublicKey::from_u256(block.signature_public_key_);
+ }
+ result.signature_ = Signature::from_u512(block.signature_);
+ result.prev_block_hash_ = block.prev_block_hash_;
+ auto change_from_tl = [&](auto &obj) {
+ return Change::from_tl(*obj);
+ };
+ result.changes_ = td::transform(block.changes_, change_from_tl);
+ result.height_ = block.height_;
+ return result;
+}
+
+td::Result<Block> Block::from_tl_serialized(td::Slice new_block) {
+ td::TlParser parser(new_block);
+ auto magic = parser.fetch_int();
+ if (magic != td::e2e_api::e2e_chain_block::ID) {
+ return td::Status::Error(PSLICE() << "Expected magic " << td::format::as_hex(td::e2e_api::e2e_chain_block::ID)
+ << ", but received " << td::format::as_hex(magic));
+ }
+ auto block_tl = td::e2e_api::e2e_chain_block::fetch(parser);
+ parser.fetch_end();
+ TRY_STATUS(parser.get_status());
+ return from_tl(*block_tl);
+}
+
+e2e::object_ptr<e2e::e2e_chain_block> Block::to_tl() const {
+ td::int32 flags{};
+ auto state_proof = state_proof_.to_tl();
+ td::UInt256 public_key{};
+ if (o_signature_public_key_) {
+ public_key = o_signature_public_key_.value().to_u256();
+ flags |= e2e::e2e_chain_block::SIGNATURE_PUBLIC_KEY_MASK;
+ }
+ auto changes = td::transform(changes_, [](const auto &change) { return change.to_tl(); });
+
+ return e2e::make_object<e2e::e2e_chain_block>(signature_.to_u512(), flags, prev_block_hash_, std::move(changes),
+ height_, std::move(state_proof), public_key);
+}
+
+std::string Block::to_tl_serialized() const {
+ return serialize_boxed(*to_tl());
+}
+
+td::StringBuilder &operator<<(td::StringBuilder &sb, const Block &block) {
+ return sb << "Block(sign=" << block.signature_
+ << "..., prev_hash=" << hex_encode(block.prev_block_hash_.as_slice().substr(0, 8))
+ << "\theight=" << block.height_ << " \n"
+ << "\tproof=" << block.state_proof_ << "\n"
+ << "\tchanges=" << block.changes_ << "\n"
+ << "\tsignature_key=" << block.o_signature_public_key_ << ")";
+}
+
+td::Result<BitString> key_to_bitstring(td::Slice key) {
+ if (key.size() != 32) {
+ return td::Status::Error("Invalid key size");
+ }
+ return BitString(key);
+}
+
+td::Result<std::string> KeyValueState::get_value(td::Slice key) const {
+ TRY_RESULT(bitstring, key_to_bitstring(key));
+ return get(node_, bitstring, snapshot_.value());
+}
+
+td::Result<std::string> KeyValueState::gen_proof(td::Span<td::Slice> keys) const {
+ TRY_RESULT(pruned_tree, generate_pruned_tree(node_, keys, snapshot_.value()));
+ return TrieNode::serialize_for_network(pruned_tree);
+}
+
+td::Result<KeyValueState> KeyValueState::create_from_hash(KeyValueHash hash) {
+ auto node = std::make_shared<TrieNode>(hash.hash);
+ return KeyValueState{std::move(node), td::Slice()};
+}
+
+td::Result<KeyValueState> KeyValueState::create_from_snapshot(td::Slice snapshot) {
+ TRY_RESULT(node, TrieNode::fetch_from_snapshot(snapshot));
+ return KeyValueState{std::move(node), snapshot};
+}
+
+td::Result<std::string> KeyValueState::build_snapshot() const {
+ return TrieNode::serialize_for_snapshot(node_, snapshot_.value());
+}
+
+td::Status KeyValueState::set_value(td::Slice key, td::Slice value) {
+ TRY_RESULT(bitstring, key_to_bitstring(key));
+ TRY_RESULT_ASSIGN(node_, set(node_, bitstring, value, snapshot_.value()));
+ return td::Status::OK();
+}
+
+td::UInt256 KeyValueState::get_hash() const {
+ return node_->hash;
+}
+
+StateProof StateProof::from_tl(const td::e2e_api::e2e_chain_stateProof &proof) {
+ StateProof res;
+ res.kv_hash = KeyValueHash{proof.kv_hash_};
+ if (proof.group_state_) {
+ res.o_group_state = GroupState::from_tl(*proof.group_state_);
+ }
+ if (proof.shared_key_) {
+ res.o_shared_key = GroupSharedKey::from_tl(*proof.shared_key_);
+ }
+ return res;
+}
+
+e2e::object_ptr<e2e::e2e_chain_stateProof> StateProof::to_tl() const {
+ td::int32 flags{};
+ e2e::object_ptr<e2e::e2e_chain_groupState> o_group_state_tl;
+ if (o_group_state) {
+ o_group_state_tl = o_group_state.value()->to_tl();
+ flags |= td::e2e_api::e2e_chain_stateProof::GROUP_STATE_MASK;
+ }
+ e2e::object_ptr<e2e::e2e_chain_sharedKey> o_shared_key_tl;
+ if (o_shared_key) {
+ o_shared_key_tl = o_shared_key.value()->to_tl();
+ flags |= td::e2e_api::e2e_chain_stateProof::SHARED_KEY_MASK;
+ }
+
+ return e2e::make_object<e2e::e2e_chain_stateProof>(flags, kv_hash.hash, std::move(o_group_state_tl),
+ std::move(o_shared_key_tl));
+}
+
+td::StringBuilder &operator<<(td::StringBuilder &sb, const StateProof &state) {
+ sb << "StateProof{";
+ sb << "\n\tkv=" << td::format::as_hex_dump<0>(state.kv_hash.hash.as_slice().substr(0, 8));
+ if (state.o_group_state) {
+ sb << "\n\tgroup=" << **state.o_group_state;
+ }
+ if (state.o_shared_key) {
+ sb << "\n\tgroup=" << **state.o_shared_key;
+ }
+ return sb << "}";
+}
+
+State State::create_empty() {
+ return State{KeyValueState{}, GroupState::empty_state(), GroupSharedKey::empty_shared_key()};
+}
+
+td::Status State::set_value(td::Slice key, td::Slice value, const Permissions &permissions) {
+ if (!permissions.may_set_value()) {
+ return Error(E::InvalidBlock_NoPermissions, "Can't set value");
+ }
+ return key_value_state_.set_value(key, value);
+}
+
+td::Status State::set_value_fast(const KeyValueHash &key_value_hash) {
+ TRY_RESULT_ASSIGN(key_value_state_, KeyValueState::create_from_hash(key_value_hash));
+ return td::Status::OK();
+}
+
+td::Status State::validate_group_state(const GroupStateRef &group_state) {
+ std::set<td::int64> new_user_ids;
+ std::set<PublicKey> new_keys;
+ for (const auto &p : group_state->participants) {
+ new_user_ids.insert(p.user_id);
+ new_keys.insert(p.public_key);
+ if ((p.flags & ~GroupParticipantFlags::AllPermissions) != 0) {
+ return Error(E::InvalidBlock_InvalidGroupState, "invalid permissions");
+ }
+ }
+ if ((group_state->external_permissions & ~GroupParticipantFlags::AllPermissions) != 0) {
+ return Error(E::InvalidBlock_InvalidGroupState, "invalid external permissions");
+ }
+ if (new_user_ids.size() != group_state->participants.size()) {
+ return Error(E::InvalidBlock_InvalidGroupState, "duplicate user_id");
+ }
+ if (new_keys.size() != group_state->participants.size()) {
+ return Error(E::InvalidBlock_InvalidGroupState, "duplicate public_key");
+ }
+ return td::Status::OK();
+}
+
+td::Status State::set_group_state(GroupStateRef group_state, const Permissions &permissions) {
+ TRY_STATUS(validate_group_state(group_state));
+
+ std::map<std::pair<td::int64, PublicKey>, td::int32> old_participants;
+ std::map<std::pair<td::int64, PublicKey>, td::int32> new_participants;
+
+ for (const auto &p : group_state_->participants) {
+ old_participants[std::make_pair(p.user_id, p.public_key)] = p.flags;
+ }
+ for (const auto &p : group_state->participants) {
+ new_participants[std::make_pair(p.user_id, p.public_key)] = p.flags;
+ }
+ if ((~group_state_->external_permissions & group_state->external_permissions) != 0) {
+ return Error(E::InvalidBlock_NoPermissions, "Can't increase external permissions");
+ }
+
+ td::int32 needed_flags = 0;
+ for (const auto &[p, flags] : old_participants) {
+ if (!new_participants.count(p)) {
+ if (!permissions.may_remove_users()) {
+ return Error(E::InvalidBlock_NoPermissions, "Can't remove users");
+ }
+ }
+ }
+ for (const auto &[p, flags] : new_participants) {
+ auto old_p = old_participants.find(p);
+ if (old_p == old_participants.end()) {
+ if (!permissions.may_add_users()) {
+ return Error(E::InvalidBlock_NoPermissions, "Can't add users");
+ }
+ needed_flags |= flags;
+ } else if (flags != old_p->second) {
+ if (!permissions.may_add_users() || !permissions.may_remove_users()) {
+ return Error(E::InvalidBlock_NoPermissions, "Can't add users");
+ }
+ needed_flags |= flags & ~old_p->second;
+ }
+ }
+
+ td::int32 missing_flags = needed_flags & ~(permissions.flags & GroupParticipantFlags::AllPermissions);
+ if (missing_flags != 0) {
+ return Error(E::InvalidBlock_NoPermissions, "Can't give more permissions than we have");
+ }
+ group_state_ = std::move(group_state);
+ return td::Status::OK();
+}
+
+td::Status State::clear_shared_key(const Permissions &permissions) {
+ if (!permissions.may_change_shared_key()) {
+ return Error(E::InvalidBlock_NoPermissions, "Can't clear shared key");
+ }
+ shared_key_ = GroupSharedKey::empty_shared_key();
+ return td::Status::OK();
+}
+
+td::Status State::validate_shared_key(const GroupSharedKeyRef &shared_key, const GroupStateRef &group_state) {
+ if (shared_key->empty_shared_key()) {
+ return td::Status::OK();
+ }
+ if (shared_key->dest_user_id.size() != shared_key->dest_header.size()) {
+ return td::Status::Error("Shared key different number of users and headers");
+ }
+ if (shared_key->dest_user_id.size() != group_state->participants.size()) {
+ return td::Status::Error("Shared key has wrong number of users");
+ }
+ std::set<td::int64> participants;
+ for (const auto user_id : shared_key->dest_user_id) {
+ participants.insert(user_id);
+ }
+ if (participants.size() != shared_key->dest_user_id.size()) {
+ return td::Status::Error("Shared key has duplicate users");
+ }
+ for (auto &p : group_state->participants) {
+ if (!participants.count(p.user_id)) {
+ return td::Status::Error("Unknown user_id in SetSharedKey");
+ }
+ }
+ return td::Status::OK();
+}
+
+td::Status State::set_shared_key(GroupSharedKeyRef shared_key, const Permissions &permissions) {
+ if (*shared_key_ != *GroupSharedKey::empty_shared_key()) {
+ return td::Status::Error("Shared key is already set");
+ }
+ if (!permissions.may_change_shared_key()) {
+ return Error(E::InvalidBlock_NoPermissions, "Can't set shared key");
+ }
+ TRY_STATUS(validate_shared_key(shared_key, group_state_));
+ shared_key_ = std::move(shared_key);
+ return td::Status::OK();
+}
+
+td::Status State::validate_state(const StateProof &state_proof) const {
+ if (state_proof.kv_hash.hash != key_value_state_.get_hash()) {
+ return td::Status::Error("State hash mismatch");
+ }
+
+ if (!has_group_state_change_ && !has_set_value_) {
+ return Error(E::InvalidBlock_NoChanges, "There must be at least SetValue or SetGroupState changes");
+ }
+ if (has_group_state_change_ && state_proof.o_group_state) {
+ return Error(E::InvalidBlock_InvalidStateProof_Group,
+ "Group state must be omitted when there is a group state change");
+ }
+ if (!has_group_state_change_ && !state_proof.o_group_state) {
+ return Error(E::InvalidBlock_InvalidStateProof_Group,
+ "Group state must be provided when there is no group state change");
+ }
+ if (!has_group_state_change_ && **state_proof.o_group_state != *group_state_) {
+ return Error(E::InvalidBlock_InvalidStateProof_Group, "group state differs");
+ }
+
+ bool shared_key_must_be_omitted = has_group_state_change_ || has_shared_key_change_;
+ if (shared_key_must_be_omitted && state_proof.o_shared_key) {
+ return Error(E::InvalidBlock_InvalidStateProof_Secret, "Shared key state must be omitted");
+ }
+ if (!shared_key_must_be_omitted && !state_proof.o_shared_key) {
+ return Error(E::InvalidBlock_InvalidStateProof_Secret, "Shared key state must be provided");
+ }
+ if (!shared_key_must_be_omitted && **state_proof.o_shared_key != *shared_key_) {
+ return Error(E::InvalidBlock_InvalidStateProof_Secret, "shared key state differs");
+ }
+
+ TRY_STATUS(validate_group_state(group_state_));
+ TRY_STATUS(validate_shared_key(shared_key_, group_state_));
+
+ return td::Status::OK();
+}
+
+td::Status State::apply_change(const Change &change_outer, const PublicKey &public_key,
+ const ValidateOptions &validate_options) {
+ bool full_apply = validate_options.validate_state_hash;
+ auto limit_permissions = validate_options.permissions;
+ return std::visit(
+ td::overloaded(
+ [](const ChangeNoop &change) { return td::Status::OK(); },
+ [this, full_apply, limit_permissions, &public_key](const ChangeSetValue &change) {
+ has_set_value_ = true;
+ if (full_apply) {
+ return set_value(change.key, change.value, group_state_->get_permissions(public_key, limit_permissions));
+ }
+ return td::Status::OK();
+ },
+ [this, limit_permissions, &public_key](const ChangeSetGroupState &change) {
+ has_group_state_change_ = true;
+ TRY_STATUS(
+ set_group_state(change.group_state, group_state_->get_permissions(public_key, limit_permissions)));
+ return clear_shared_key(group_state_->get_permissions(public_key, limit_permissions));
+ },
+ [this, limit_permissions, &public_key](const ChangeSetSharedKey &change) {
+ has_shared_key_change_ = true;
+ return set_shared_key(change.shared_key, group_state_->get_permissions(public_key, limit_permissions));
+ }),
+ change_outer.value);
+}
+
+td::Status State::apply(Block &block, ValidateOptions validate_options) {
+ // To apply the first block an ephemeral -1 block is used
+ // - It has only one participant - Participant(user_id = 0, public_key = signer_public_key, permissions = all)
+ if (block.height_ == 0) {
+ CHECK(group_state_->empty());
+ group_state_ = std::make_unique<GroupState>(GroupState{{}, GroupParticipantFlags::AllPermissions});
+ }
+
+ td::optional<PublicKey> o_signature_public_key = block.o_signature_public_key_;
+ if (!o_signature_public_key && !group_state_->empty()) {
+ o_signature_public_key = group_state_->participants[0].public_key;
+ }
+ if (!o_signature_public_key) {
+ return td::Status::Error("Unknown public key");
+ }
+
+ // 5. Verifies the signature of the block.
+ if (validate_options.validate_signature) {
+ TRY_STATUS(block.verify_signature(o_signature_public_key.value()));
+ }
+
+ // 6. Applies the changes to the state.
+ // - If `validate_state_hash` is true, the state hash is validated.
+ // - Otherwise, the state hash is set to the hash of the block.
+ has_set_value_ = false;
+ has_shared_key_change_ = false;
+ has_group_state_change_ = false;
+ for (auto &change : block.changes_) {
+ TRY_STATUS(apply_change(change, o_signature_public_key.value(), validate_options));
+ }
+ if (!validate_options.validate_state_hash) {
+ TRY_STATUS(set_value_fast(block.state_proof_.kv_hash));
+ }
+
+ TRY_STATUS(validate_state(block.state_proof_));
+
+ return td::Status::OK();
+}
+
+td::Result<State> State::create_from_block(const Block &block, td::optional<td::Slice> o_snapshot) {
+ KeyValueState key_value_state;
+ GroupStateRef group_state;
+ GroupSharedKeyRef shared_key;
+
+ if (o_snapshot) {
+ TRY_RESULT_ASSIGN(key_value_state, KeyValueState::create_from_snapshot(o_snapshot.value()));
+ } else {
+ TRY_RESULT_ASSIGN(key_value_state, KeyValueState::create_from_hash(block.state_proof_.kv_hash));
+ }
+
+ // For the first block we fixup group state.
+ if (block.height_ == 0) {
+ group_state = std::make_shared<GroupState>(GroupState{{}, GroupParticipantFlags::AllPermissions});
+ }
+
+ bool has_set_value = false;
+ bool has_group_state_change = false;
+ bool has_shared_key_change = false;
+ for (const auto &change_v : block.changes_) {
+ std::visit(
+ td::overloaded([](const ChangeNoop &change) {}, [&](const ChangeSetValue &change) { has_set_value = true; },
+ [&](const ChangeSetGroupState &change) {
+ group_state = change.group_state;
+ shared_key = GroupSharedKey::empty_shared_key();
+ has_group_state_change = true;
+ },
+ [&](const ChangeSetSharedKey &change) {
+ shared_key = change.shared_key;
+ has_shared_key_change = true;
+ }),
+ change_v.value);
+ }
+
+ if (block.state_proof_.o_group_state) {
+ group_state = block.state_proof_.o_group_state.value();
+ }
+ if (block.state_proof_.o_shared_key) {
+ shared_key = block.state_proof_.o_shared_key.value();
+ }
+ if (!group_state) {
+ return Error(E::InvalidBlock_InvalidStateProof_Group, "no group state proof");
+ }
+ if (!shared_key) {
+ return Error(E::InvalidBlock_InvalidStateProof_Secret, "no shared key");
+ }
+
+ auto state = State(key_value_state, group_state, shared_key);
+ state.has_set_value_ = has_set_value;
+ state.has_group_state_change_ = has_group_state_change;
+ state.has_shared_key_change_ = has_shared_key_change;
+ TRY_STATUS(state.validate_state(block.state_proof_));
+ return state;
+}
+
+td::Result<Block> Blockchain::build_block(std::vector<Change> changes, const PrivateKey &private_key) const {
+ //TODO: check if we are allowed to sign this block
+ auto public_key = private_key.to_public_key();
+ auto state = state_;
+ if (last_block_.height_ == std::numeric_limits<td::int32>::max()) {
+ return td::Status::Error("Blockchain::build_block: last block height is too high");
+ }
+ td::int32 height = last_block_.height_ + 1;
+ if (height == 0) {
+ state.group_state_ = std::make_shared<GroupState>(GroupState{{}, GroupParticipantFlags::AllPermissions});
+ }
+
+ ValidateOptions validate_options;
+ validate_options.validate_state_hash = true;
+ validate_options.validate_signature = false;
+ validate_options.permissions = GroupParticipantFlags::AllPermissions;
+ for (const auto &change : changes) {
+ TRY_STATUS(state.apply_change(change, public_key, validate_options));
+ }
+
+ StateProof state_proof;
+ state_proof.kv_hash = KeyValueHash{state.key_value_state_.get_hash()};
+ state_proof.o_group_state = state.group_state_;
+ state_proof.o_shared_key = state.shared_key_;
+ state.has_set_value_ = false;
+ state.has_group_state_change_ = false;
+ state.has_shared_key_change_ = false;
+ for (const auto &change_v : changes) {
+ std::visit(td::overloaded([](const ChangeNoop &change) {},
+ [&](const ChangeSetValue &change) { state.has_set_value_ = true; },
+ [&](const ChangeSetGroupState &change) {
+ state_proof.o_group_state = {};
+ state_proof.o_shared_key = {};
+ state.has_group_state_change_ = true;
+ },
+ [&](const ChangeSetSharedKey &change) {
+ state_proof.o_shared_key = {};
+ state.has_shared_key_change_ = true;
+ }),
+ change_v.value);
+ }
+ TRY_STATUS(state.validate_state(state_proof));
+
+ Block block;
+ block.height_ = height;
+ block.prev_block_hash_ = last_block_hash_;
+ block.changes_ = std::move(changes);
+ block.o_signature_public_key_ = public_key;
+ block.state_proof_ = std::move(state_proof);
+ TRY_STATUS(block.sign_inplace(private_key));
+ return block;
+}
+
+td::Status Blockchain::try_apply_block(Block block, ValidateOptions validate_options) {
+ // To apply the first block an ephemeral -1 block is used
+ // - It has hash UInt256(0)
+ // - It has height -1
+ // - It has only one participant - Participant(user_id = 0, public_key = signer_public_key, permissions = all)
+
+ if (block.height_ != get_height() + 1 || get_height() == std::numeric_limits<td::int32>::max()) {
+ return Error(E::InvalidBlock_HeightMismatch,
+ PSLICE() << "new_block.height=" << block.height_ << " != 1 + last_block.height=" << get_height());
+ }
+
+ if (block.prev_block_hash_ != last_block_hash_) {
+ return Error(E::InvalidBlock_HashMismatch);
+ }
+
+ // TODO: validate total size of block
+ auto state = state_;
+ TRY_STATUS(state.apply(block, validate_options));
+
+ // NO errors after this point
+ state_ = std::move(state);
+
+ last_block_hash_ = block.calc_hash();
+
+ last_block_ = std::move(block);
+ return td::Status::OK();
+}
+
+Block Blockchain::set_value(td::Slice key, td::Slice value, const PrivateKey &private_key) const {
+ return build_block({Change{ChangeSetValue{key.str(), value.str()}}}, private_key).move_as_ok();
+}
+
+td::int64 Blockchain::get_height() const {
+ return last_block_.height_;
+}
+
+td::Result<td::UInt256> as_key(td::Slice key) {
+ if (key.size() != 32) {
+ return td::Status::Error("Invalid key size");
+ }
+ td::UInt256 key_int256;
+ key_int256.as_mutable_slice().copy_from(key);
+ if (key_int256.is_zero()) {
+ return td::Status::Error("Invalid zero key");
+ }
+ return key_int256;
+}
+
+td::Result<Blockchain> Blockchain::create_from_block(Block block, td::optional<td::Slice> o_snapshot) {
+ if (block.height_ < 0) {
+ return Error(E::InvalidBlock, "negative height");
+ }
+ Blockchain res;
+ res.last_block_hash_ = block.calc_hash();
+ TRY_RESULT_ASSIGN(res.state_, State::create_from_block(block, std::move(o_snapshot)));
+ res.last_block_ = std::move(block);
+
+ return res;
+}
+
+namespace {
+bool is_good_magic(td::int32 magic) {
+ return magic == td::e2e_api::e2e_chain_block::ID || magic == td::e2e_api::e2e_chain_groupBroadcastNonceCommit::ID ||
+ magic == td::e2e_api::e2e_chain_groupBroadcastNonceReveal::ID;
+}
+} // namespace
+
+bool Blockchain::is_from_server(td::Slice block) {
+ if (block.size() < 4) {
+ return false;
+ }
+ td::int32 server_magic = td::as<td::int32>(block.data());
+ return is_good_magic(server_magic - 1) && !is_good_magic(server_magic);
+}
+
+td::Result<std::string> Blockchain::from_any_to_local(std::string block) {
+ if (is_from_server(block)) {
+ return from_server_to_local(std::move(block));
+ }
+ return block;
+}
+
+td::Result<std::string> Blockchain::from_server_to_local(std::string block) {
+ if (block.size() < 4) {
+ return td::Status::Error("Block is too short");
+ }
+ td::int32 server_magic = td::as<td::int32>(block.data());
+ if (is_good_magic(server_magic)) {
+ return td::Status::Error("Trying to apply local block, not from server");
+ }
+ td::int32 real_magic = server_magic - 1;
+ td::as<td::int32>(block.data()) = real_magic;
+ return block;
+}
+
+td::Result<std::string> Blockchain::from_local_to_server(std::string block) {
+ if (block.size() < 4) {
+ return td::Status::Error("Block is too short");
+ }
+ td::int32 magic = td::as<td::int32>(block.data());
+ td::as<td::int32>(block.data()) = magic + 1;
+ return block;
+}
+
+td::Result<ClientBlockchain> ClientBlockchain::create_from_block(td::Slice block_slice, const PublicKey &public_key) {
+ TRY_RESULT(block, Block::from_tl_serialized(block_slice));
+ TRY_RESULT(blockchain, Blockchain::create_from_block(std::move(block)));
+ ClientBlockchain res;
+ res.blockchain_ = std::move(blockchain);
+ return res;
+}
+
+td::Result<ClientBlockchain> ClientBlockchain::create_empty() {
+ ClientBlockchain res;
+ res.blockchain_ = Blockchain::create_empty();
+ return res;
+}
+
+td::Result<std::vector<Change>> ClientBlockchain::try_apply_block(td::Slice block_slice) {
+ TRY_RESULT(block, Block::from_tl_serialized(block_slice));
+
+ ValidateOptions validate_options;
+ validate_options.validate_signature = true;
+ validate_options.validate_state_hash = false;
+ TRY_STATUS(blockchain_.try_apply_block(block, validate_options));
+ for (auto &change : block.changes_) {
+ if (std::holds_alternative<ChangeSetValue>(change.value)) {
+ auto &change_value = std::get<ChangeSetValue>(change.value);
+ auto key = as_key(change_value.key).move_as_ok(); // already verified in try_apply_block
+ map_[key] = Entry{block.height_, change_value.value};
+ }
+ }
+
+ return std::move(block.changes_);
+}
+
+td::Status ClientBlockchain::add_proof(td::Slice proof) {
+ TRY_RESULT(state, TrieNode::fetch_from_network(proof));
+
+ if (state->hash != blockchain_.state_.key_value_state_.get_hash()) {
+ return td::Status::Error("Invalid proof");
+ }
+ // TODO: merge proof
+ blockchain_.state_.key_value_state_.node_ = state;
+ return td::Status::OK();
+}
+
+td::Result<std::string> ClientBlockchain::build_block(const std::vector<Change> &changes,
+ const PrivateKey &private_key) const {
+ TRY_RESULT(block, blockchain_.build_block(changes, private_key));
+ return block.to_tl_serialized();
+}
+
+td::Result<std::string> ClientBlockchain::get_value(td::Slice raw_key) const {
+ TRY_RESULT(key, as_key(raw_key));
+ auto it = map_.find(key);
+ if (it != map_.end()) {
+ return it->second.value;
+ }
+ return blockchain_.state_.key_value_state_.get_value(raw_key);
+}
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/Blockchain.h b/tde2e/td/e2e/Blockchain.h
new file mode 100644
index 000000000..fad35e91e
--- /dev/null
+++ b/tde2e/td/e2e/Blockchain.h
@@ -0,0 +1,351 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include "td/e2e/Trie.h"
+#include "td/e2e/utils.h"
+
+#include "td/telegram/e2e_api.h"
+
+#include "td/utils/common.h"
+#include "td/utils/FlatHashMap.h"
+#include "td/utils/optional.h"
+#include "td/utils/Slice.h"
+#include "td/utils/Span.h"
+#include "td/utils/Status.h"
+#include "td/utils/StringBuilder.h"
+#include "td/utils/UInt.h"
+
+#include <memory>
+#include <variant>
+
+namespace tde2e_core {
+
+namespace e2e = td::e2e_api;
+
+struct KeyValueHash {
+ td::UInt256 hash{};
+};
+
+enum GroupParticipantFlags : td::int32 {
+ AddUsers = 1 << 0,
+ RemoveUsers = 1 << 1,
+ SetValue = 1 << 2,
+ AllPermissions = (1 << 3) - 1,
+ IsParticipant = 1 << 30
+};
+
+struct GroupParticipant {
+ td::int64 user_id{0};
+ td::int32 flags{0};
+ PublicKey public_key{};
+ td::int32 version{0};
+ bool add_users() const {
+ return (flags & GroupParticipantFlags::AddUsers) != 0;
+ }
+ bool remove_users() const {
+ return (flags & GroupParticipantFlags::RemoveUsers) != 0;
+ }
+ bool operator==(const GroupParticipant &other) const {
+ return user_id == other.user_id && flags == other.flags && public_key == other.public_key &&
+ version == other.version;
+ }
+ bool operator!=(const GroupParticipant &other) const {
+ return !(other == *this);
+ }
+
+ static GroupParticipant from_tl(const td::e2e_api::e2e_chain_groupParticipant &participant);
+ e2e::object_ptr<e2e::e2e_chain_groupParticipant> to_tl() const;
+};
+
+inline td::StringBuilder &operator<<(td::StringBuilder &sb, const GroupParticipant &part) {
+ return sb << "(uid=" << part.user_id << ", flags=" << part.flags << ", pk=" << part.public_key
+ << ", version=" << part.version << ")";
+}
+
+struct GroupState;
+struct GroupSharedKey;
+using GroupStateRef = std::shared_ptr<const GroupState>;
+using GroupSharedKeyRef = std::shared_ptr<const GroupSharedKey>;
+
+struct Permissions {
+ td::int32 flags{0};
+ bool may_add_users() const {
+ return (flags & GroupParticipantFlags::AddUsers) != 0;
+ }
+ bool may_remove_users() const {
+ return (flags & GroupParticipantFlags::RemoveUsers) != 0;
+ }
+ bool may_set_value() const {
+ return (flags & GroupParticipantFlags::SetValue) != 0;
+ }
+ bool is_participant() const {
+ return (flags & GroupParticipantFlags::IsParticipant) != 0;
+ }
+ bool may_change_shared_key() const {
+ return is_participant() && (may_remove_users() || may_add_users());
+ }
+};
+
+struct GroupState {
+ std::vector<GroupParticipant> participants;
+ td::int32 external_permissions{};
+ bool empty() const {
+ return participants.empty();
+ }
+ td::int32 version() const;
+ td::Result<GroupParticipant> get_participant(td::int64 user_id) const;
+ td::Result<GroupParticipant> get_participant(const PublicKey &public_key) const;
+ Permissions get_permissions(const PublicKey &public_key, td::int32 limit_permissions) const;
+ static GroupStateRef from_tl(const td::e2e_api::e2e_chain_groupState &state);
+ e2e::object_ptr<e2e::e2e_chain_groupState> to_tl() const;
+ static GroupStateRef empty_state();
+ bool operator==(const GroupState &other) const {
+ return participants == other.participants && external_permissions == other.external_permissions;
+ }
+ bool operator!=(const GroupState &other) const {
+ return !(other == *this);
+ }
+};
+inline td::StringBuilder &operator<<(td::StringBuilder &sb, const GroupState &state) {
+ return sb << state.participants << ", external_permissions=" << state.external_permissions;
+}
+
+struct GroupSharedKey {
+ PublicKey ek;
+ std::string encrypted_shared_key;
+ std::vector<td::int64> dest_user_id;
+ std::vector<std::string> dest_header;
+ static GroupSharedKeyRef from_tl(const td::e2e_api::e2e_chain_sharedKey &shared_key);
+ e2e::object_ptr<e2e::e2e_chain_sharedKey> to_tl() const;
+ static GroupSharedKeyRef empty_shared_key();
+ bool empty() const {
+ return *this == *empty_shared_key();
+ }
+ bool operator==(const GroupSharedKey &other) const {
+ return ek == other.ek && encrypted_shared_key == other.encrypted_shared_key && dest_user_id == other.dest_user_id &&
+ dest_header == other.dest_header;
+ }
+ bool operator!=(const GroupSharedKey &other) const {
+ return !(other == *this);
+ }
+};
+
+inline td::StringBuilder &operator<<(td::StringBuilder &sb, const GroupSharedKey &shared_key) {
+ return sb << "SharedKey{uids=" << shared_key.dest_user_id << "}";
+}
+
+struct ChangeNoop {
+ td::UInt256 nonce;
+ static ChangeNoop from_tl(const td::e2e_api::e2e_chain_changeNoop &change) {
+ return ChangeNoop{change.nonce_};
+ }
+ e2e::object_ptr<e2e::e2e_chain_changeNoop> to_tl() const {
+ return e2e::make_object<e2e::e2e_chain_changeNoop>(nonce);
+ }
+};
+inline td::StringBuilder &operator<<(td::StringBuilder &sb, const ChangeNoop &change) {
+ return sb << "Noop{}";
+}
+
+struct ChangeSetValue {
+ std::string key;
+ std::string value;
+ static ChangeSetValue from_tl(const td::e2e_api::e2e_chain_changeSetValue &change);
+ e2e::object_ptr<e2e::e2e_chain_changeSetValue> to_tl() const;
+};
+inline td::StringBuilder &operator<<(td::StringBuilder &sb, const ChangeSetValue &state) {
+ return sb << "SetValue{key.size=" << state.key.size() << ", value.size=" << state.value.size() << "}";
+}
+struct ChangeSetGroupState {
+ GroupStateRef group_state;
+ static ChangeSetGroupState from_tl(const td::e2e_api::e2e_chain_changeSetGroupState &change);
+ e2e::object_ptr<e2e::e2e_chain_changeSetGroupState> to_tl() const;
+};
+inline td::StringBuilder &operator<<(td::StringBuilder &sb, const ChangeSetGroupState &state) {
+ return sb << "SetGroupState{" << *state.group_state << "}";
+}
+struct ChangeSetSharedKey {
+ GroupSharedKeyRef shared_key;
+ static ChangeSetSharedKey from_tl(const td::e2e_api::e2e_chain_changeSetSharedKey &change);
+ e2e::object_ptr<e2e::e2e_chain_changeSetSharedKey> to_tl() const;
+};
+inline td::StringBuilder &operator<<(td::StringBuilder &sb, const ChangeSetSharedKey &shared_key) {
+ return sb << "SetSharedKey{" << *shared_key.shared_key << "}";
+}
+
+struct Change {
+ std::variant<ChangeSetValue, ChangeSetGroupState, ChangeSetSharedKey, ChangeNoop> value;
+ static Change from_tl(const td::e2e_api::e2e_chain_Change &change);
+ e2e::object_ptr<e2e::e2e_chain_Change> to_tl() const;
+};
+inline td::StringBuilder &operator<<(td::StringBuilder &sb, const Change &change) {
+ std::visit([&](auto &value) { sb << value; }, change.value);
+ return sb;
+}
+
+struct KeyValueState {
+ TrieRef node_{TrieNode::empty_node()};
+ td::optional<td::Slice> snapshot_{td::Slice()};
+ td::Result<std::string> get_value(td::Slice key) const;
+ td::Result<std::string> gen_proof(td::Span<td::Slice> keys) const;
+ static td::Result<KeyValueState> create_from_hash(KeyValueHash hash);
+ static td::Result<KeyValueState> create_from_snapshot(td::Slice snapshot);
+ td::Result<std::string> build_snapshot() const;
+ td::UInt256 get_hash() const;
+ td::Status set_value(td::Slice key, td::Slice value);
+ //td::Status set_value_fast(KeyValueHash key_value_hash);
+};
+
+struct StateProof {
+ KeyValueHash kv_hash;
+ td::optional<GroupStateRef> o_group_state;
+ td::optional<GroupSharedKeyRef> o_shared_key;
+ static StateProof from_tl(const td::e2e_api::e2e_chain_stateProof &proof);
+ e2e::object_ptr<e2e::e2e_chain_stateProof> to_tl() const;
+};
+
+td::StringBuilder &operator<<(td::StringBuilder &sb, const StateProof &state);
+struct ValidateOptions {
+ bool validate_state_hash{true};
+ bool validate_signature{true};
+ td::int32 permissions{GroupParticipantFlags::AllPermissions};
+};
+
+struct Block;
+struct State {
+ KeyValueState key_value_state_;
+ GroupStateRef group_state_;
+ GroupSharedKeyRef shared_key_;
+ bool has_set_value_{};
+ bool has_shared_key_change_{};
+ bool has_group_state_change_{};
+
+ State() = default;
+ State(KeyValueState key_value_state, GroupStateRef group_state, GroupSharedKeyRef shared_key)
+ : key_value_state_(std::move(key_value_state))
+ , group_state_(std::move(group_state))
+ , shared_key_(std::move(shared_key)) {
+ CHECK(group_state_);
+ CHECK(shared_key_);
+ }
+
+ static State create_empty();
+ static td::Result<State> create_from_block(const Block &block, td::optional<td::Slice> o_snapshot = {});
+
+ td::Status set_value(td::Slice key, td::Slice value, const Permissions &permissions);
+ td::Status set_group_state(GroupStateRef group_state, const Permissions &permissions);
+ td::Status clear_shared_key(const Permissions &permissions);
+ td::Status set_shared_key(GroupSharedKeyRef shared_key, const Permissions &permissions);
+ td::Status set_value_fast(const KeyValueHash &key_value_hash);
+ td::Status apply_change(const Change &change_outer, const PublicKey &public_key, const ValidateOptions &options);
+
+ td::Status apply(Block &block, ValidateOptions validate_options = {});
+
+ td::Status validate_state(const StateProof &state_proof) const;
+
+ static td::Status validate_group_state(const GroupStateRef &group_state);
+ static td::Status validate_shared_key(const GroupSharedKeyRef &shared_key, const GroupStateRef &group_state);
+};
+
+struct Block {
+ Signature signature_;
+ td::UInt256 prev_block_hash_{};
+ std::vector<Change> changes_;
+ td::int32 height_{-1};
+
+ StateProof state_proof_;
+ td::optional<PublicKey> o_signature_public_key_;
+
+ td::Status sign_inplace(const PrivateKey &private_key) {
+ TRY_RESULT_ASSIGN(signature_, ::tde2e_core::sign(private_key, *to_tl()));
+ return td::Status::OK();
+ }
+ td::Status verify_signature(const PublicKey &public_key) const {
+ return ::tde2e_core::verify_signature(public_key, *to_tl());
+ }
+ td::UInt256 calc_hash() const;
+
+ static td::Result<Block> from_tl_serialized(td::Slice new_block);
+ std::string to_tl_serialized() const;
+
+ private:
+ e2e::object_ptr<e2e::e2e_chain_block> to_tl() const;
+ static Block from_tl(const e2e::e2e_chain_block &block);
+};
+td::StringBuilder &operator<<(td::StringBuilder &sb, const Block &block);
+
+struct Blockchain {
+ static Blockchain create_empty() {
+ return Blockchain{Block{}, td::UInt256{}, State::create_empty()};
+ }
+ static td::Result<Blockchain> create_from_block(Block block, td::optional<td::Slice> o_snapshot = {});
+
+ static bool is_from_server(td::Slice block);
+ static td::Result<std::string> from_any_to_local(std::string block);
+ static td::Result<std::string> from_server_to_local(std::string block);
+ static td::Result<std::string> from_local_to_server(std::string block);
+
+ td::Result<Block> build_block(std::vector<Change> changes, const PrivateKey &private_key) const;
+ td::Status try_apply_block(Block block, ValidateOptions validate_options);
+ Block set_value(td::Slice key, td::Slice value, const PrivateKey &private_key) const;
+ td::int64 get_height() const;
+
+ Block last_block_;
+ td::UInt256 last_block_hash_{};
+ State state_{State::create_empty()};
+
+ void attach_snapshot(td::Slice snapshot) {
+ state_.key_value_state_.snapshot_ = snapshot;
+ }
+ void detach_snapshot() {
+ state_.key_value_state_.snapshot_ = td::Slice();
+ }
+};
+
+class ClientBlockchain {
+ public:
+ static td::Result<ClientBlockchain> create_from_block(td::Slice block_slice, const PublicKey &public_key);
+ static td::Result<ClientBlockchain> create_empty();
+
+ td::Result<std::vector<Change>> try_apply_block(td::Slice block_slice);
+
+ td::int64 get_height() const {
+ return blockchain_.get_height();
+ }
+ td::UInt256 get_last_block_hash() const {
+ return blockchain_.last_block_hash_;
+ }
+ td::UInt256 get_previous_block_hash() const {
+ return blockchain_.last_block_.prev_block_hash_;
+ }
+
+ td::Status add_proof(td::Slice proof);
+
+ td::Result<std::string> build_block(const std::vector<Change> &changes, const PrivateKey &private_key) const;
+
+ td::Result<std::string> get_value(td::Slice key) const;
+ GroupSharedKeyRef get_group_shared_key() const {
+ return blockchain_.state_.shared_key_;
+ }
+ GroupStateRef get_group_state() const {
+ return blockchain_.state_.group_state_;
+ }
+ const Blockchain &get_inner_chain() const {
+ return blockchain_;
+ }
+
+ private:
+ Blockchain blockchain_;
+ struct Entry {
+ td::int64 height;
+ std::string value;
+ };
+ td::FlatHashMap<td::UInt256, Entry, UInt256Hash> map_;
+};
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/Blockchain.md b/tde2e/td/e2e/Blockchain.md
new file mode 100644
index 000000000..b60e27b8c
--- /dev/null
+++ b/tde2e/td/e2e/Blockchain.md
@@ -0,0 +1,194 @@
+# Blockchain Implementation Documentation
+
+## Overview
+
+The blockchain implementation provides a distributed ledger system that maintains a consistent state across multiple participants. It supports key-value storage, participant management, and secure state transitions. The blockchain is designed with security in mind, ensuring only valid blocks with proper signatures and correct heights can be applied.
+
+## Core Components
+
+### Block Structure
+
+As defined in the e2e_api.tl scheme:
+```
+e2e.chain.stateProof flags:# kv_hash:int256 group_state:flags.0?e2e.chain.GroupState shared_key:flags.1?e2e.chain.SharedKey = e2e.chain.StateProof;
+
+e2e.chain.block signature:int512 flags:# prev_block_hash:int256 changes:vector<e2e.chain.Change> height:int state_proof:e2e.chain.StateProof signature_public_key:flags.0?int256 = e2e.chain.Block;
+```
+
+A block consists of:
+- **Signature**: Cryptographic signature verifying the block's authenticity
+- **Previous Block Hash**: Links to the previous block, creating a chain
+- **Changes**: A vector of operations to apply to the blockchain state
+- **Height**: Sequential block number, critical for validation
+- **State Proof**: Contains hashes and states for validation. This proof represents the state of the blockchain after the block was applied.
+- **Signature Public Key**: The key of the participant who created the block
+
+### Signature Generation
+
+The signature is generated for the TL-serialized block with the signature field zeroed.
+
+### Block Hash Generation
+
+The hash of the block is the SHA256 of the TL-serialized block.
+
+### Change Types
+
+The blockchain supports four types of changes:
+
+1. **ChangeSetValue**: Updates a key-value pair in the blockchain
+ ```
+ e2e.chain.changeSetValue key:bytes value:bytes = e2e.chain.Change;
+ ```
+
+2. **ChangeSetGroupState**: Updates the group of participants and their permissions
+ ```
+ e2e.chain.groupParticipant user_id:long public_key:int256 flags:# add_users:flags.0?true remove_users:flags.1?true version:int = e2e.chain.GroupParticipant;
+ e2e.chain.groupState participants:vector<e2e.chain.GroupParticipant> = e2e.chain.GroupState;
+ e2e.chain.changeSetGroupState group_state:e2e.chain.GroupState = e2e.chain.Change;
+ ```
+
+3. **ChangeSetSharedKey**: Updates the encryption keys shared among participants
+ ```
+ e2e.chain.sharedKey ek:int256 encrypted_shared_key:string dest_user_id:vector<long> dest_header:vector<bytes> = e2e.chain.SharedKey;
+ e2e.chain.changeSetSharedKey shared_key:e2e.chain.SharedKey = e2e.chain.Change;
+ ```
+
+4. **ChangeNoop**: Does nothing and can be used for hash randomization. Currently, it must be present in the zero block.
+ ```
+ e2e.chain.changeNoop random:int256 = e2e.chain.Change;
+ ```
+
+### Participants and Permissions
+
+Participants in the blockchain have specific permissions:
+- **AddUsers**: Can add new participants to the blockchain
+- **RemoveUsers**: Can remove existing participants from the blockchain
+
+```
+e2e.chain.groupParticipant user_id:long public_key:int256 flags:# add_users:flags.0?true remove_users:flags.1?true version:int = e2e.chain.GroupParticipant;
+```
+
+### Implementation Details
+
+#### Key-Value State
+
+The blockchain uses a persistent trie for key-value storage, with the following properties:
+- Supports set/get operations
+- Generates pruned trees for a given set of keys
+- A pruned tree allows:
+ - `get` operations for any of the specified keys
+ - `set` operations for any of those keys to create a new (pruned) trie
+
+```c++
+td::Result<TrieRef> set(TrieRef n, BitString key, td::Slice value, td::Slice snapshot = {});
+td::Result<std::string> get(const TrieRef &n, BitString key, td::Slice snapshot = {});
+td::Result<TrieRef> generate_pruned_tree(const TrieRef &n, td::Span<td::Slice> keys, td::Slice snapshot = {});
+```
+
+The trie can be serialized for network transmission or persistent storage:
+
+```c++
+static td::Result<std::string> serialize_for_network(TrieRef node);
+static td::Result<TrieRef> fetch_from_network(td::Slice data);
+static td::Result<std::string> serialize_for_snapshot(TrieRef node, td::Slice snapshot);
+static td::Result<TrieRef> fetch_from_snapshot(td::Slice snapshot);
+```
+
+- `{serialize_for,fetch_from}_network` is used for passing a pruned trie over the network
+- `{serialize_for,fetch_from}_snapshot` is used by the server to persist the entire state to disk
+
+#### Blockchain State
+
+The complete blockchain state consists of:
+- A trie (TrieRef root + Slice snapshot) for key-value storage
+- A group state (participants and their permissions)
+- Shared key information (encryption keys shared among participants)
+
+## Expected Behaviors
+
+### Block Application Process
+
+A block is either applied completely or not at all.
+
+1. The block's height is checked. It must be exactly one more than the current blockchain height.
+ - If the height is incorrect, the block is rejected with `HEIGHT_MISMATCH`
+2. The hash of the previous block is checked. It must match the hash of the last applied block.
+ - If the hash is incorrect, the block is rejected with `PREVIOUS_BLOCK_HASH_MISMATCH`
+3. The permissions of the participant who created the block (the one with `signer_public_key`) are determined.
+ - First, we look for the signer's public key in the previous state. If found, we use its permissions; otherwise, we use external_permissions
+4. The block signature is verified.
+ - If the signature is invalid, the block is rejected with `INVALID_SIGNATURE`
+5. Next, changes from the block are applied one by one.
+ - Before applying a change, we check that the participant has sufficient permissions to apply it.
+ - Then, the change is applied.
+ - After applying a block, the block creator's permissions could be updated. This is important because any subsequent changes should be applied using the new permissions. The idea is that applying changes in the same block should yield the same result as applying them in separate blocks.
+ - If any change is invalid, the block is rejected with the corresponding error.
+6. After all changes are applied, the block's state proof must be valid for the new state.
+ - If the state proof is invalid, the block is rejected with `INVALID_STATE_PROOF`
+
+To apply the first block, an ephemeral block with height `-1` is used:
+ - It has a hash of `UInt256(0)`
+ - Its height is `-1`
+ - It has effective (but not explicitly stored, i.e., not reflected in its hash) `self_join_permissions` with all permissions
+
+There are also several optimizations for block serialization:
+ - The `signer_public_key` can be omitted if it is the same as the public key of the first participant in the group state
+ - `group_state` in `state_proof` can be omitted if there is a `SetGroupState` change in the block
+ - `shared_key` in `state_proof` can be omitted if there is a `SetSharedKey` or `SetGroupState` change in the block
+
+### Applying Changes
+
+The idea is that applying changes within the same block should lead to the same state as applying them in multiple blocks.
+
+#### Key Value Updates
+
+Currently, any participant can update any key with a new value. This change is always successful. Deletion is the same as overwriting with an empty value.
+
+- The trie is updated with the new value
+- The trie hash must be stored in the new state proof
+
+### Participant Management
+
+- Only participants with the `AddUsers` permission can add new participants
+- A participant may add users with permissions that are a non-strict subset of its own permissions
+- As an exception, it is possible to give permissions to another user
+- Only participants with the `RemoveUsers` permission can remove existing participants
+- Both the public key and user ID are unique in the group state
+- Any new state of the group is allowed otherwise
+- The shared key is automatically cleared by this change
+
+#### Shared Key Updates
+
+- The shared key cannot be overwritten by other participants. One must update the group state to clear the key first.
+- The shared key is automatically cleared by a `SetGroupState` change.
+- The shared key must contain all user_ids of all participants, and only them.
+- How the shared key is encrypted is not the blockchain's concern.
+- Only participants may update the key.
+
+Note:
+- It is impossible to create a new key if the user is not in the group, even if it is an automatic removal of the key
+- Only participants may create a new key
+- It is impossible to remove yourself from the group (this could be allowed in the future, but would lead to an empty shared key)
+
+## Known Behaviors and Considerations
+
+### Multiple Blocks at the Same Height
+
+If two blocks are built concurrently for the same height:
+- Only the first applied block will succeed
+- The second block will be rejected with `HEIGHT_MISMATCH`
+- This is intended behavior to avoid forks and force all changes to be applied **exactly** in the way the creator intended
+
+### Partial State Handling
+
+The client library does not store the entire key-value state. To create a block, the client must receive a proof of all changed keys from the server.
+
+### Security
+
+There are several aspects we should be particularly careful about:
+
+1. Clients must apply only blocks received from the server. This is especially important for blocks created by the client itself. The server does extra work to ensure correctness of blocks and to prevent forks. Forks per se are not a security problem, but they would lead to broken calls.
+
+2. Blocks should be sent to the server until a response is received. This could be either success or error. In case of an error like INVALID_BLOCK__HASH_MISMATCH, a new block could be created, but one should be careful about how the group state has been changed.
+
+3. Broadcast blocks should also be sent until they are accepted or declined.
diff --git a/tde2e/td/e2e/Call.cpp b/tde2e/td/e2e/Call.cpp
new file mode 100644
index 000000000..c467329c8
--- /dev/null
+++ b/tde2e/td/e2e/Call.cpp
@@ -0,0 +1,792 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/e2e/Call.h"
+
+#include "td/e2e/e2e_api.h"
+#include "td/e2e/MessageEncryption.h"
+#include "td/e2e/Mnemonic.h"
+
+#include "td/telegram/e2e_api.hpp"
+#include "td/utils/algorithm.h"
+
+#include "td/utils/as.h"
+#include "td/utils/common.h"
+#include "td/utils/crypto.h"
+#include "td/utils/logging.h"
+#include "td/utils/misc.h"
+#include "td/utils/overloaded.h"
+#include "td/utils/Random.h"
+#include "td/utils/SliceBuilder.h"
+#include "td/utils/tl_helpers.h"
+#include "td/utils/tl_parsers.h"
+
+#include <algorithm>
+#include <limits>
+#include <memory>
+#include <tuple>
+#include <utility>
+
+namespace tde2e_core {
+
+CallVerificationChain::State CallVerificationChain::get_state() const {
+ return state_;
+}
+
+template <class F>
+struct LambdaStorer {
+ const F &store_;
+};
+
+template <class F, class StorerT>
+void store(const LambdaStorer<F> &lambda_storer, StorerT &storer) {
+ lambda_storer.store_(storer);
+}
+
+template <class F>
+std::string lambda_serialize(F &&f) {
+ return td::serialize(LambdaStorer<F>{std::forward<F>(f)});
+}
+
+void CallVerificationChain::on_new_main_block(const Blockchain &blockhain) {
+ state_ = Commit;
+ CHECK(blockhain.get_height() > height_);
+ height_ = td::narrow_cast<td::int32>(blockhain.get_height());
+ last_block_hash_ = blockhain.last_block_hash_;
+ verification_state_ = {};
+ verification_state_.height = height_;
+
+ verification_words_ =
+ CallVerificationWords{height_, Mnemonic::generate_verification_words(last_block_hash_.as_slice())};
+ auto &group_state = *blockhain.state_.group_state_;
+ committed_ = {};
+ revealed_ = {};
+
+ participant_keys_ = {};
+ for (auto &participant : group_state.participants) {
+ participant_keys_.emplace(participant.user_id, participant.public_key);
+ }
+ CHECK(participant_keys_.size() == group_state.participants.size());
+
+ commit_at_ = td::Timestamp::now();
+ reveal_at_ = {};
+ done_at_ = {};
+ users_ = {};
+ for (auto &participant : group_state.participants) {
+ users_[participant.user_id];
+ }
+
+ if (auto it = delayed_broadcasts_.find(height_); it != delayed_broadcasts_.end()) {
+ for (auto &[message, broadcast] : it->second) {
+ auto status = process_broadcast(std::move(message), std::move(broadcast));
+ LOG_IF(ERROR, status.is_error()) << "Failed to process broadcast: " << status;
+ }
+ delayed_broadcasts_.erase(it);
+ }
+}
+
+td::Status CallVerificationChain::try_apply_block(td::Slice message) {
+ // parse e2e::e2e_chain_GroupBroadcast
+ td::TlParser parser(message);
+ auto kv_broadcast = e2e::e2e_chain_GroupBroadcast::fetch(parser);
+ parser.fetch_end();
+ TRY_STATUS(parser.get_status());
+
+ td::int32 chain_height{-1};
+ downcast_call(*kv_broadcast, td::overloaded([&](auto &broadcast) { chain_height = broadcast.chain_height_; }));
+
+ if (chain_height < height_) {
+ LOG(INFO) << "Skip old broadcast " << to_short_string(kv_broadcast);
+ // broadcast is too old
+ return td::Status::OK();
+ }
+
+ if (chain_height > height_) {
+ if (!delay_allowed_) {
+ return Error(E::InvalidBroadcast_InFuture, PSLICE()
+ << "broadcast_height=" << chain_height << " height=" << height_);
+ }
+
+ LOG(INFO) << "Delay broadcast " << to_short_string(kv_broadcast);
+ delayed_broadcasts_[chain_height].emplace_back(message.str(), std::move(kv_broadcast));
+ return td::Status::OK();
+ }
+
+ return process_broadcast(message.str(), std::move(kv_broadcast));
+}
+
+std::string CallVerificationChain::to_short_string(e2e::object_ptr<e2e::e2e_chain_GroupBroadcast> &broadcast) {
+ td::StringBuilder sb;
+ downcast_call(*broadcast,
+ td::overloaded([&](e2e::e2e_chain_groupBroadcastNonceCommit &commit) { sb << "CommitBroadcast"; },
+ [&](e2e::e2e_chain_groupBroadcastNonceReveal &reveal) { sb << "RevealBroadcast"; }));
+ downcast_call(*broadcast, [&](auto &v) {
+ sb << "{height=" << v.chain_height_ << " user_id=" << v.user_id_;
+ auto it = participant_keys_.find(v.user_id_);
+ if (it != participant_keys_.end()) {
+ sb << " pk=" << it->second;
+ } else {
+ sb << " pk=?";
+ }
+ sb << "}";
+ });
+ return sb.as_cslice().str();
+}
+
+td::Status CallVerificationChain::process_broadcast(std::string message,
+ e2e::object_ptr<e2e::e2e_chain_GroupBroadcast> broadcast) {
+ td::Status status;
+ td::UInt256 broadcast_chain_hash{};
+ downcast_call(*broadcast, td::overloaded([&](auto &broadcast) { broadcast_chain_hash = broadcast.chain_hash_; }));
+ if (broadcast_chain_hash != last_block_hash_) {
+ status = Error(E::InvalidBroadcast_InvalidBlockHash);
+ }
+ if (status.is_ok()) {
+ downcast_call(
+ *broadcast,
+ td::overloaded([&](e2e::e2e_chain_groupBroadcastNonceCommit &commit) { status = process_broadcast(commit); },
+ [&](e2e::e2e_chain_groupBroadcastNonceReveal &reveal) { status = process_broadcast(reveal); }));
+ }
+
+ if (status.is_error()) {
+ LOG(ERROR) << "Failed broadcast\n" << to_short_string(broadcast) << "\n\t" << status;
+ } else {
+ LOG(DEBUG) << "Applied broadcast\n\t" << to_short_string(broadcast) << "\n\t" << *this;
+ }
+ return status;
+}
+
+CallVerificationState CallVerificationChain::get_verification_state() const {
+ return verification_state_;
+}
+
+CallVerificationWords CallVerificationChain::get_verification_words() const {
+ return verification_words_;
+}
+
+td::Status CallVerificationChain::process_broadcast(e2e::e2e_chain_groupBroadcastNonceCommit &nonce_commit) {
+ CHECK(nonce_commit.chain_height_ == height_);
+ if (state_ != Commit) {
+ return Error(E::InvalidBroadcast_NotInCommit);
+ }
+ auto user_id = nonce_commit.user_id_;
+ auto it = participant_keys_.find(user_id);
+ if (it == participant_keys_.end()) {
+ return Error(E::InvalidBroadcast_UnknownUserId);
+ }
+ auto public_key = it->second;
+ if (!may_skip_signatures_validation_) {
+ TRY_STATUS(verify_signature(public_key, nonce_commit));
+ }
+
+ if (committed_.count(user_id) != 0) {
+ return Error(E::InvalidBroadcast_AlreadyApplied);
+ }
+
+ committed_[user_id] = nonce_commit.nonce_hash_.as_slice().str();
+ users_[user_id].receive_commit_at_ = td::Timestamp::now();
+
+ if (committed_.size() == participant_keys_.size()) {
+ state_ = Reveal;
+ reveal_at_ = td::Timestamp::now();
+ }
+
+ return td::Status::OK();
+}
+
+td::Status CallVerificationChain::process_broadcast(e2e::e2e_chain_groupBroadcastNonceReveal &nonce_reveal) {
+ CHECK(nonce_reveal.chain_height_ == height_);
+ if (state_ != Reveal) {
+ return Error(E::InvalidBroadcast_NotInReveal);
+ }
+ auto user_id = nonce_reveal.user_id_;
+ auto user_id_it = participant_keys_.find(user_id);
+ if (user_id_it == participant_keys_.end()) {
+ return Error(E::InvalidBroadcast_UnknownUserId);
+ }
+ auto public_key = user_id_it->second;
+ if (!may_skip_signatures_validation_) {
+ TRY_STATUS(verify_signature(public_key, nonce_reveal));
+ }
+
+ if (revealed_.count(user_id) != 0) {
+ return Error(E::InvalidBroadcast_AlreadyApplied);
+ }
+
+ auto it = committed_.find(user_id);
+ CHECK(it != committed_.end());
+ auto expected_nonce_hash = it->second;
+ auto received_nonce_hash = td::sha256(nonce_reveal.nonce_.as_slice());
+ if (expected_nonce_hash != received_nonce_hash) {
+ return Error(E::InvalidBroadcast_InvalidReveal);
+ }
+
+ revealed_[user_id] = nonce_reveal.nonce_.as_slice().str();
+ users_[user_id].receive_reveal_at_ = td::Timestamp::now();
+
+ CHECK(!verification_state_.emoji_hash);
+ if (revealed_.size() == participant_keys_.size()) {
+ auto nonces = td::transform(revealed_, [](auto &p) { return p.second; });
+ std::sort(nonces.begin(), nonces.end());
+
+ std::string full_nonce;
+ for (auto &nonce : nonces) {
+ full_nonce += nonce;
+ }
+
+ verification_state_.emoji_hash =
+ MessageEncryption::hmac_sha512(full_nonce, last_block_hash_.as_slice()).as_slice().str();
+ state_ = End;
+ done_at_ = td::Timestamp::now();
+ }
+ return td::Status::OK();
+}
+
+CallEncryption::CallEncryption(td::int64 user_id, PrivateKey private_key)
+ : user_id_(user_id), private_key_(std::move(private_key)) {
+}
+
+td::Status CallEncryption::add_shared_key(td::int32 epoch, td::UInt256 epoch_hash, td::SecureString key,
+ GroupStateRef group_state) {
+ sync();
+
+ TRY_RESULT(self, group_state->get_participant(private_key_.to_public_key()));
+ if (self.user_id != user_id_) {
+ // should not happen
+ return td::Status::Error("Wrong user identifier in state");
+ }
+
+ LOG(INFO) << "Add key from epoch: " << epoch;
+ epoch_by_hash_[epoch_hash] = epoch;
+ auto added =
+ epochs_.emplace(epoch, EpochInfo(epoch, epoch_hash, self.user_id, std::move(key), std::move(group_state))).second;
+ CHECK(added);
+ return td::Status::OK();
+}
+
+void CallEncryption::forget_shared_key(td::int32 epoch, td::UInt256 epoch_hash) {
+ sync();
+ epochs_to_forget_.emplace(td::Timestamp::in(FORGET_EPOCH_DELAY), epoch);
+}
+
+td::Result<std::string> CallEncryption::decrypt(td::int64 user_id, td::int32 channel_id, td::Slice encrypted_data) {
+ sync();
+ if (user_id == user_id_) {
+ return td::Status::Error("Packet is encrypted by us");
+ }
+ td::TlParser parser(encrypted_data);
+ auto head = static_cast<td::uint32>(parser.fetch_int());
+ td::int32 epochs_n = head & 0xff;
+ auto version = (head >> 8) & 0xff;
+ auto reserved = head >> 16;
+
+ if (version != 0) {
+ return td::Status::Error("Unsupported protocol version");
+ }
+ if (reserved != 0) {
+ return td::Status::Error("Reserved part of head is not zero");
+ }
+
+ if (epochs_n > MAX_ACTIVE_EPOCHS) {
+ return td::Status::Error("Too many active epochs");
+ }
+
+ std::vector<td::UInt256> epoch_hashes(epochs_n);
+ for (auto &epoch : epoch_hashes) {
+ parse(epoch, parser);
+ }
+ auto unencrypted_header = encrypted_data.substr(0, encrypted_data.size() - parser.get_left_len());
+
+ std::vector<td::Slice> encrypted_headers;
+ for (td::int32 i = 0; i < epochs_n; i++) {
+ auto encrypted_header = parser.template fetch_string_raw<td::Slice>(32);
+ encrypted_headers.emplace_back(encrypted_header);
+ }
+
+ auto encrypted_packet = parser.template fetch_string_raw<td::Slice>(parser.get_left_len());
+ parser.fetch_end();
+ TRY_STATUS(parser.get_status());
+
+ for (td::int32 i = 0; i < epochs_n; i++) {
+ auto epoch_hash = epoch_hashes[i];
+ auto encrypted_header = encrypted_headers[i];
+ if (auto it = epoch_by_hash_.find(epoch_hash); it != epoch_by_hash_.end()) {
+ auto it2 = epochs_.find(it->second);
+ if (it2 != epochs_.end()) {
+ auto &epoch_info = it2->second;
+ TRY_RESULT(one_time_secret,
+ MessageEncryption::decrypt_header(encrypted_header, encrypted_packet, epoch_info.secret_));
+ return decrypt_packet_with_secret(user_id, channel_id, unencrypted_header, encrypted_packet, one_time_secret,
+ epoch_info.group_state_);
+ }
+ }
+ }
+ return Error(E::Decrypt_UnknownEpoch);
+}
+
+td::Result<std::string> CallEncryption::encrypt(td::int32 channel_id, td::Slice decrypted_data) {
+ sync();
+
+ // use all active epochs
+ if (epochs_.empty()) {
+ return Error(E::Encrypt_UnknownEpoch);
+ }
+ auto epochs_n = td::narrow_cast<td::int32>(epochs_.size());
+
+ using td::store;
+ std::string header_a = lambda_serialize([&](auto &storer) {
+ store(epochs_n, storer);
+ for (auto &[epoch_i, epoch] : epochs_) {
+ store(epoch.epoch_hash_, storer);
+ }
+ });
+
+ td::SecureString one_time_secret(32, 0);
+ td::Random::secure_bytes(one_time_secret.as_mutable_slice());
+ TRY_RESULT(encrypted_packet, encrypt_packet_with_secret(channel_id, header_a, decrypted_data, one_time_secret));
+
+ std::vector<td::SecureString> encrypted_headers;
+ for (auto &[epoch_i, epoch] : epochs_) {
+ TRY_RESULT(encrypted_header, MessageEncryption::encrypt_header(one_time_secret, encrypted_packet, epoch.secret_));
+ encrypted_headers.emplace_back(std::move(encrypted_header));
+ }
+
+ std::string header_b = lambda_serialize([&](auto &storer) {
+ for (auto &encrypted_header : encrypted_headers) {
+ CHECK(encrypted_header.size() == 32);
+ storer.store_slice(encrypted_header);
+ }
+ });
+
+ //LOG(ERROR) << decrypted_data.size() << " -> " << header_a.size() << " + " << header_b.size() << " + " << encrypted_packet.size();
+ return header_a + header_b + encrypted_packet;
+}
+
+std::string add_magic(td::int32 magic, td::Slice header) {
+ std::string res(4 + header.size(), '\0');
+ td::as<td::int32>(res.data()) = magic;
+ td::MutableSlice(res).substr(4).copy_from(header);
+ return res;
+}
+
+td::Result<std::string> CallEncryption::encrypt_packet_with_secret(td::int32 channel_id, td::Slice unencrypted_part,
+ td::Slice packet, td::Slice one_time_secret) {
+ TRY_STATUS(validate_channel_id(channel_id));
+ auto &seqno = seqno_[channel_id];
+ if (seqno == std::numeric_limits<td::uint32>::max()) {
+ return td::Status::Error("Seqno overflow");
+ }
+ seqno++;
+
+ auto payload = lambda_serialize([&](auto &storer) {
+ using td::store;
+ store(static_cast<td::int32>(channel_id), storer);
+ store(seqno, storer);
+ storer.store_slice(packet);
+ });
+
+ // TODO: there is too much copies happening here. Almost all of them could be avoided
+ td::UInt256 large_msg_id{};
+ auto encrypted_payload = MessageEncryption::encrypt_data(
+ payload, one_time_secret, add_magic(td::e2e_api::e2e_callPacket::ID, unencrypted_part), &large_msg_id);
+ auto to_sign = add_magic(td::e2e_api::e2e_callPacketLargeMsgId::ID, large_msg_id.as_slice());
+
+ TRY_RESULT(signature, private_key_.sign(to_sign));
+ return encrypted_payload.as_slice().str() + signature.to_slice().str();
+}
+
+td::Result<std::string> CallEncryption::decrypt_packet_with_secret(
+ td::int64 expected_user_id, td::int32 expected_channel_id, td::Slice unencrypted_header, td::Slice encrypted_packet,
+ td::Slice one_time_secret, const GroupStateRef &group_state) {
+ TRY_RESULT(participant, group_state->get_participant(expected_user_id));
+ if (encrypted_packet.size() < 64) {
+ return td::Status::Error("Not enough encryption data");
+ }
+ TRY_RESULT(signature, Signature::from_slice(encrypted_packet.substr(encrypted_packet.size() - 64, 64)));
+ encrypted_packet.remove_suffix(64);
+
+ td::UInt256 large_msg_id{};
+ TRY_RESULT(payload_str, MessageEncryption::decrypt_data(
+ encrypted_packet, one_time_secret,
+ add_magic(td::e2e_api::e2e_callPacket::ID, unencrypted_header), &large_msg_id));
+ // we know that this is packet created by some participant
+
+ auto payload = td::Slice(payload_str);
+ auto to_verify = add_magic(td::e2e_api::e2e_callPacketLargeMsgId::ID, large_msg_id.as_slice());
+ TRY_STATUS(participant.public_key.verify(to_verify, signature));
+
+ td::TlParser parser(payload);
+ td::int32 channel_id;
+ td::uint32 seqno{};
+ parse(channel_id, parser);
+ TRY_STATUS(validate_channel_id(channel_id));
+ parse(seqno, parser);
+ auto result = parser.template fetch_string_raw<std::string>(parser.get_left_len());
+ parser.fetch_end();
+ TRY_STATUS(parser.get_status());
+
+ if (channel_id != expected_channel_id) {
+ // currently ignore expected_channel_id
+ // return td::Status::Error("Channel identifier mismatch");
+ }
+ TRY_STATUS(check_not_seen(participant.public_key, channel_id, seqno));
+ mark_as_seen(participant.public_key, channel_id, seqno);
+ return result;
+}
+
+td::Status CallEncryption::check_not_seen(const PublicKey &public_key, td::int32 channel_id, td::uint32 seqno) {
+ auto &s = seen_[std::make_pair(public_key, channel_id)];
+ if (s.empty()) {
+ return td::Status::OK();
+ }
+ auto value = seqno;
+ if (value < *s.begin()) {
+ return td::Status::Error("Message is too old");
+ }
+ if (s.count(value) != 0) {
+ return td::Status::Error("Message is already processed");
+ }
+ return td::Status::OK();
+}
+
+void CallEncryption::mark_as_seen(const PublicKey &public_key, td::int32 channel_id, td::uint32 seqno) {
+ auto value = seqno;
+ auto &s = seen_[std::make_pair(public_key, channel_id)];
+ CHECK(s.insert(value).second);
+ while (s.size() > 1024 || (!s.empty() && *s.begin() + 1024 < seqno)) {
+ s.erase(s.begin());
+ }
+}
+
+void CallEncryption::sync() {
+ auto now = td::Timestamp::now();
+ while (!epochs_to_forget_.empty() &&
+ (epochs_to_forget_.front().first.is_in_past(now) || epochs_.size() > MAX_ACTIVE_EPOCHS)) {
+ auto epoch = epochs_to_forget_.front().second;
+ LOG(INFO) << "Forget key from epoch: " << epoch;
+ auto it = epochs_.find(epoch);
+ if (it != epochs_.end()) {
+ epoch_by_hash_.erase(it->second.epoch_hash_);
+ epochs_.erase(it);
+ }
+ epochs_to_forget_.pop();
+ }
+}
+
+td::Status CallEncryption::validate_channel_id(td::int32 channel_id) {
+ if (channel_id < 0 || channel_id > 1023) {
+ return Error(E::InvalidCallChannelId);
+ }
+ return td::Status::OK();
+}
+
+CallVerification CallVerification::create(td::int64 user_id, PrivateKey private_key, const Blockchain &blockchain) {
+ CallVerification result;
+ result.user_id_ = user_id;
+ result.private_key_ = std::move(private_key);
+ result.chain_.allow_delay();
+ result.chain_.set_user_id(user_id);
+ result.on_new_main_block(blockchain);
+ return result;
+}
+
+void CallVerification::on_new_main_block(const Blockchain &blockchain) {
+ auto nonce = generate_nonce();
+ td::UInt256 nonce_hash;
+ td::sha256(nonce.as_mutable_slice(), nonce_hash.as_mutable_slice());
+
+ auto height = td::narrow_cast<td::int32>(blockchain.get_height());
+ auto last_block_hash = blockchain.last_block_hash_;
+ auto nonce_commit_tl = e2e::e2e_chain_groupBroadcastNonceCommit({}, user_id_, height, last_block_hash, nonce_hash);
+ nonce_commit_tl.signature_ = sign(private_key_, nonce_commit_tl).move_as_ok().to_u512();
+ auto nonce_commit = serialize_boxed(nonce_commit_tl);
+
+ height_ = height;
+ last_block_hash_ = blockchain.last_block_hash_;
+ nonce_ = nonce;
+ sent_commit_ = true;
+ sent_reveal_ = false;
+ pending_outbound_messages_ = {nonce_commit};
+ chain_.on_new_main_block(blockchain);
+}
+
+CallVerificationWords CallVerification::get_verification_words() const {
+ return chain_.get_verification_words();
+}
+
+CallVerificationState CallVerification::get_verification_state() const {
+ return chain_.get_verification_state();
+}
+
+std::vector<std::string> CallVerification::pull_outbound_messages() {
+ std::vector<std::string> result;
+ std::swap(result, pending_outbound_messages_);
+ return result;
+}
+
+td::Status CallVerification::receive_inbound_message(td::Slice message) {
+ TRY_STATUS(chain_.try_apply_block(message));
+
+ if (chain_.get_state() == CallVerificationChain::Reveal && !sent_reveal_) {
+ sent_reveal_ = true;
+ auto nonce_reveal_tl = e2e::e2e_chain_groupBroadcastNonceReveal({}, user_id_, height_, last_block_hash_, nonce_);
+ nonce_reveal_tl.signature_ = sign(private_key_, nonce_reveal_tl).move_as_ok().to_u512();
+ auto nonce_reveal = serialize_boxed(nonce_reveal_tl);
+ CHECK(pending_outbound_messages_.empty());
+ pending_outbound_messages_.push_back(nonce_reveal);
+ }
+ return td::Status::OK();
+}
+
+Call::Call(td::int64 user_id, PrivateKey pk, ClientBlockchain blockchain)
+ : user_id_(user_id)
+ , private_key_(std::move(pk))
+ , blockchain_(std::move(blockchain))
+ , call_encryption_(user_id, private_key_) {
+ CHECK(private_key_);
+ call_verification_ = CallVerification::create(user_id_, private_key_, blockchain_.get_inner_chain());
+ LOG(INFO) << "Create call \n" << *this;
+}
+
+td::Result<std::string> Call::create_zero_block(const PrivateKey &private_key, GroupStateRef group_state) {
+ TRY_RESULT(blockchain, ClientBlockchain::create_empty());
+ TRY_RESULT(changes, make_changes_for_new_state(std::move(group_state)));
+ return blockchain.build_block(changes, private_key);
+}
+
+td::Result<std::string> Call::create_self_add_block(const PrivateKey &private_key, td::Slice previous_block_server,
+ const GroupParticipant &self) {
+ TRY_RESULT(previous_block, Blockchain::from_server_to_local(previous_block_server.str()));
+ TRY_RESULT(blockchain, ClientBlockchain::create_from_block(previous_block, private_key.to_public_key()));
+ auto old_state = *blockchain.get_group_state();
+ td::remove_if(old_state.participants,
+ [&self](const GroupParticipant &participant) { return participant.user_id == self.user_id; });
+ old_state.participants.push_back(self);
+ auto new_group_state = std::make_shared<GroupState>(std::move(old_state));
+ TRY_RESULT(changes, make_changes_for_new_state(std::move(new_group_state)));
+ return blockchain.build_block(changes, private_key);
+}
+
+td::Result<Call> Call::create(td::int64 user_id, PrivateKey private_key, td::Slice last_block_server) {
+ TRY_RESULT(last_block, Blockchain::from_server_to_local(last_block_server.str()));
+ TRY_RESULT(blockchain, ClientBlockchain::create_from_block(last_block, private_key.to_public_key()));
+ auto call = Call(user_id, std::move(private_key), std::move(blockchain));
+ TRY_STATUS(call.update_group_shared_key());
+ return call;
+}
+
+td::Result<std::string> Call::build_change_state(GroupStateRef new_group_state) const {
+ TRY_STATUS(get_status());
+ TRY_RESULT(changes, make_changes_for_new_state(std::move(new_group_state)));
+ return blockchain_.build_block(changes, private_key_);
+}
+
+td::Result<std::vector<Change>> Call::make_changes_for_new_state(GroupStateRef group_state) {
+ TRY_RESULT(e_private_key, PrivateKey::generate());
+ td::SecureString group_shared_key(32);
+ td::Random::secure_bytes(group_shared_key.as_mutable_slice());
+
+ td::SecureString one_time_secret(32);
+ td::Random::secure_bytes(one_time_secret.as_mutable_slice());
+
+ auto encrypted_group_shared_key = MessageEncryption::encrypt_data(group_shared_key, one_time_secret);
+
+ std::vector<td::int64> dst_user_id;
+ std::vector<std::string> dst_header;
+ for (auto &participant : group_state->participants) {
+ auto public_key = participant.public_key;
+ TRY_RESULT(shared_key, e_private_key.compute_shared_secret(public_key));
+ dst_user_id.push_back(participant.user_id);
+ TRY_RESULT(header, MessageEncryption::encrypt_header(one_time_secret, encrypted_group_shared_key, shared_key));
+ dst_header.push_back(header.as_slice().str());
+ }
+ auto change_set_shared_key = Change{ChangeSetSharedKey{std::make_shared<GroupSharedKey>(
+ GroupSharedKey{e_private_key.to_public_key(), encrypted_group_shared_key.as_slice().str(), std::move(dst_user_id),
+ std::move(dst_header)})}};
+ auto change_set_group_state = Change{ChangeSetGroupState{std::move(group_state)}};
+
+ return std::vector<Change>{std::move(change_set_group_state), std::move(change_set_shared_key)};
+}
+
+td::Result<td::int32> Call::get_height() const {
+ TRY_STATUS(get_status());
+ return td::narrow_cast<td::int32>(blockchain_.get_height());
+}
+
+td::Result<GroupStateRef> Call::get_group_state() const {
+ TRY_STATUS(get_status());
+ return blockchain_.get_group_state();
+}
+
+td::Status Call::apply_block(td::Slice server_block) {
+ TRY_STATUS(get_status());
+ TRY_RESULT(block, Blockchain::from_server_to_local(server_block.str()));
+ auto status = do_apply_block(block);
+ if (status.is_error()) {
+ LOG(ERROR) << "Failed to apply block: " << status << "\n" << Block::from_tl_serialized(block);
+ status_ = std::move(status);
+ } else {
+ LOG(INFO) << "Block has been applied\n" << *this;
+ }
+
+ return get_status();
+}
+td::Status Call::do_apply_block(td::Slice block) {
+ TRY_RESULT(changes, blockchain_.try_apply_block(block));
+ call_verification_.on_new_main_block(blockchain_.get_inner_chain());
+ TRY_STATUS(update_group_shared_key());
+ return td::Status::OK();
+}
+
+td::Result<td::SecureString> Call::decrypt_shared_key() {
+ auto group_shared_key = blockchain_.get_group_shared_key();
+ for (size_t i = 0; i < group_shared_key->dest_user_id.size(); i++) {
+ if (group_shared_key->dest_user_id[i] == user_id_) {
+ TRY_RESULT(shared_key, private_key_.compute_shared_secret(group_shared_key->ek));
+ TRY_RESULT(one_time_secret,
+ MessageEncryption::decrypt_header(group_shared_key->dest_header[i],
+ group_shared_key->encrypted_shared_key, shared_key));
+ TRY_RESULT(decrypted_shared_key,
+ MessageEncryption::decrypt_data(group_shared_key->encrypted_shared_key, one_time_secret));
+ if (decrypted_shared_key.size() != 32) {
+ return td::Status::Error("Invalid shared key (size != 32)");
+ }
+ group_shared_key_ = td::SecureString(
+ MessageEncryption::hmac_sha512(group_shared_key_, blockchain_.get_last_block_hash().as_slice())
+ .as_slice()
+ .substr(0, 32));
+ return decrypted_shared_key;
+ }
+ }
+ return td::Status::Error("Could not find user_id in group_shared_key");
+}
+
+td::Status Call::update_group_shared_key() {
+ // NB: we drop key immediately, we don't want old key to be active due to some errors later
+ group_shared_key_ = {};
+ call_encryption_.forget_shared_key(td::narrow_cast<td::int32>(blockchain_.get_height() - 1),
+ blockchain_.get_previous_block_hash());
+
+ auto group_state = blockchain_.get_group_state();
+
+ auto r_participant = group_state->get_participant(private_key_.to_public_key());
+ if (r_participant.is_error()) {
+ return Error(E::InvalidCallGroupState_NotParticipant);
+ }
+ auto participant = r_participant.move_as_ok();
+ if (participant.user_id != user_id_) {
+ return Error(E::InvalidCallGroupState_WrongUserId);
+ }
+
+ TRY_RESULT_ASSIGN(group_shared_key_, decrypt_shared_key());
+
+ return call_encryption_.add_shared_key(td::narrow_cast<td::int32>(blockchain_.get_height()),
+ blockchain_.get_last_block_hash(), group_shared_key_.copy(), group_state);
+}
+
+td::StringBuilder &operator<<(td::StringBuilder &sb, const CallVerificationChain &chain) {
+ sb << "Verification {height=" << chain.height_ << " state=";
+ switch (chain.state_) {
+ case CallVerificationChain::State::Commit:
+ sb << "commit";
+ break;
+ case CallVerificationChain::State::Reveal:
+ sb << "reveal";
+ break;
+ case CallVerificationChain::State::End:
+ sb << "done";
+ break;
+ }
+ sb << " commit_n=" << chain.committed_.size() << " reveal_n=" << chain.revealed_.size() << "}";
+ auto now = td::Timestamp::now();
+ sb << "\n\t\t";
+ sb << "commit->";
+ if (chain.state_ == CallVerificationChain::State::Commit) {
+ sb << (now.at() - chain.commit_at_.at()) << "s->...";
+ } else {
+ sb << (chain.reveal_at_.at() - chain.commit_at_.at()) << "s->reveal->";
+ if (chain.state_ == CallVerificationChain::State::Reveal) {
+ sb << (now.at() - chain.reveal_at_.at()) << "s->...";
+ } else {
+ sb << (chain.done_at_.at() - chain.reveal_at_.at()) << "s->done";
+ }
+ }
+ auto it = chain.users_.find(chain.user_id_);
+ if (it != chain.users_.end()) {
+ const CallVerificationChain::UserState &self = it->second;
+ sb << "\n\t\tself:";
+ if (self.receive_commit_at_) {
+ sb << " commit=" << self.receive_commit_at_.at() - chain.commit_at_.at() << "s";
+ } else {
+ sb << " commit=" << now.at() - chain.commit_at_.at() << "s...";
+ }
+ if (chain.state_ != CallVerificationChain::State::Commit) {
+ if (self.receive_reveal_at_) {
+ sb << " reveal=" << self.receive_reveal_at_.at() - chain.reveal_at_.at() << "s";
+ } else {
+ sb << " reveal=" << now.at() - chain.reveal_at_.at() << "s...";
+ }
+ }
+ }
+
+ {
+ sb << "\n\t\t";
+ sb << "commit =";
+ auto users = td::transform(chain.users_, [&](auto &key) {
+ auto t = chain.users_.at(key.first).receive_commit_at_;
+ if (t) {
+ return std::make_tuple(-(t.at() - chain.commit_at_.at()), key.first, false);
+ }
+ return std::make_tuple(-(now.at() - chain.commit_at_.at()), key.first, true);
+ });
+ std::sort(users.begin(), users.end());
+ for (auto &user : users) {
+ sb << " " << std::get<1>(user) << ":" << -std::get<0>(user) << "s";
+ if (std::get<2>(user)) {
+ sb << "...";
+ }
+ }
+ }
+ if (chain.state_ != CallVerificationChain::State::Commit) {
+ sb << "\n\t\t";
+ sb << "reveal =";
+ auto users = td::transform(chain.users_, [&](auto &key) {
+ auto t = chain.users_.at(key.first).receive_reveal_at_;
+ if (t) {
+ return std::make_tuple(-(t.at() - chain.reveal_at_.at()), key.first, false);
+ }
+ return std::make_tuple(-(now.at() - chain.reveal_at_.at()), key.first, true);
+ });
+ std::sort(users.begin(), users.end());
+ for (auto &user : users) {
+ sb << " " << std::get<1>(user) << ":" << -std::get<0>(user) << "s";
+ if (std::get<2>(user)) {
+ sb << "...";
+ }
+ }
+ }
+
+ return sb;
+}
+
+td::StringBuilder &operator<<(td::StringBuilder &sb, const CallVerification &verification) {
+ return sb << verification.chain_;
+}
+
+td::StringBuilder &operator<<(td::StringBuilder &sb, const Call &call) {
+ auto status = call.get_status();
+ sb << "Call{" << call.blockchain_.get_height() << ":" << call.private_key_.to_public_key() << "}";
+ if (status.is_error()) {
+ sb << "\nCALL_FAILED: " << call.status_;
+ }
+ auto group_state = call.blockchain_.get_group_state();
+ sb << "\n\tusers=" << td::transform(group_state->participants, [](auto &p) { return p.user_id; });
+ sb << "\n\tpkeys=" << td::transform(group_state->participants, [](auto &p) { return p.public_key; });
+ sb << "\n\t" << call.call_verification_;
+ return sb;
+}
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/Call.h b/tde2e/td/e2e/Call.h
new file mode 100644
index 000000000..ab04b7272
--- /dev/null
+++ b/tde2e/td/e2e/Call.h
@@ -0,0 +1,235 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include "td/e2e/Blockchain.h"
+#include "td/e2e/Container.h"
+#include "td/e2e/e2e_api.h"
+
+#include "td/utils/SharedSlice.h"
+#include "td/utils/Slice.h"
+#include "td/utils/SliceBuilder.h"
+#include "td/utils/Status.h"
+#include "td/utils/StringBuilder.h"
+#include "td/utils/Time.h"
+#include "td/utils/UInt.h"
+#include "td/utils/VectorQueue.h"
+
+#include <map>
+#include <set>
+#include <utility>
+
+namespace tde2e_core {
+
+using tde2e_api::CallVerificationState;
+using tde2e_api::CallVerificationWords;
+
+struct CallVerificationChain {
+ enum State {
+ End,
+ Commit,
+ Reveal,
+ };
+ State get_state() const;
+ void on_new_main_block(const Blockchain &blockhain);
+ td::Status try_apply_block(td::Slice message);
+ std::string to_short_string(e2e::object_ptr<e2e::e2e_chain_GroupBroadcast> &broadcast);
+
+ CallVerificationState get_verification_state() const;
+ CallVerificationWords get_verification_words() const;
+
+ void set_user_id(td::int64 user_id) {
+ user_id_ = user_id;
+ }
+ void allow_delay() {
+ delay_allowed_ = true;
+ }
+ void skip_signatures_validation() {
+ may_skip_signatures_validation_ = true;
+ }
+
+ friend td::StringBuilder &operator<<(td::StringBuilder &sb, const CallVerificationChain &chain);
+
+ private:
+ td::Status process_broadcast(std::string message, e2e::object_ptr<e2e::e2e_chain_GroupBroadcast> broadcast);
+ td::Status process_broadcast(e2e::e2e_chain_groupBroadcastNonceCommit &nonce_commit);
+ td::Status process_broadcast(e2e::e2e_chain_groupBroadcastNonceReveal &nonce_reveal);
+
+ State state_{End};
+ CallVerificationState verification_state_;
+ CallVerificationWords verification_words_;
+ td::int32 height_{-1};
+ td::UInt256 last_block_hash_{};
+ std::map<td::int64, PublicKey> participant_keys_;
+ std::map<td::int64, std::string> committed_;
+ std::map<td::int64, std::string> revealed_;
+
+ td::int64 user_id_{};
+
+ td::Timestamp commit_at_{};
+ td::Timestamp reveal_at_{};
+ td::Timestamp done_at_{};
+ struct UserState {
+ td::Timestamp receive_commit_at_{};
+ td::Timestamp receive_reveal_at_{};
+ };
+ std::map<td::int64, UserState> users_;
+
+ bool delay_allowed_{false};
+ bool may_skip_signatures_validation_{false};
+ std::map<td::int32, std::vector<std::pair<std::string, e2e::object_ptr<e2e::e2e_chain_GroupBroadcast>>>>
+ delayed_broadcasts_;
+};
+
+class CallEncryption {
+ public:
+ CallEncryption(td::int64 user_id, PrivateKey private_key);
+ td::Status add_shared_key(td::int32 epoch, td::UInt256 epoch_hash, td::SecureString key, GroupStateRef group_state);
+ void forget_shared_key(td::int32 epoch, td::UInt256 epoch_hash);
+
+ td::Result<std::string> decrypt(td::int64 expected_user_id, td::int32 expected_channel_id, td::Slice encrypted_data);
+ td::Result<std::string> encrypt(td::int32 channel_id, td::Slice decrypted_data);
+
+ private:
+ static constexpr double FORGET_EPOCH_DELAY = 10;
+ static constexpr td::int32 MAX_ACTIVE_EPOCHS = 15;
+ td::int64 user_id_{};
+ PrivateKey private_key_;
+
+ struct EpochInfo {
+ EpochInfo(td::int32 epoch, td::UInt256 epoch_hash, td::int64 user_id, td::SecureString secret,
+ GroupStateRef group_state)
+ : epoch_(epoch)
+ , epoch_hash_(epoch_hash)
+ , user_id_(user_id)
+ , secret_(std::move(secret))
+ , group_state_(std::move(group_state)) {
+ }
+
+ td::int32 epoch_{};
+ td::UInt256 epoch_hash_{};
+ td::int64 user_id_{};
+ td::SecureString secret_;
+ GroupStateRef group_state_;
+ };
+
+ std::map<td::int32, td::uint32> seqno_;
+ std::map<td::int32, EpochInfo> epochs_;
+ std::map<td::UInt256, td::int32> epoch_by_hash_;
+ td::VectorQueue<std::pair<td::Timestamp, td::int32>> epochs_to_forget_;
+ std::map<std::pair<PublicKey, td::int32>, std::set<td::uint32>> seen_;
+
+ void sync();
+
+ td::Result<std::string> encrypt_packet_with_secret(td::int32 channel_id, td::Slice header, td::Slice packet,
+ td::Slice one_time_secret);
+ td::Result<std::string> decrypt_packet_with_secret(td::int64 expected_user_id, td::int32 expected_channel_id,
+ td::Slice unencrypted_packet, td::Slice encrypted_packet,
+ td::Slice one_time_secret, const GroupStateRef &group_state);
+ td::Status check_not_seen(const PublicKey &public_key, td::int32 channel_id, td::uint32 seqno);
+ void mark_as_seen(const PublicKey &public_key, td::int32 channel_id, td::uint32 seqno);
+ static td::Status validate_channel_id(td::int32 channel_id);
+};
+
+class CallVerification {
+ public:
+ static CallVerification create(td::int64 user_id, PrivateKey private_key, const Blockchain &blockchain);
+ void on_new_main_block(const Blockchain &blockhain);
+ CallVerificationState get_verification_state() const;
+ std::vector<std::string> pull_outbound_messages();
+ CallVerificationWords get_verification_words() const;
+ td::Status receive_inbound_message(td::Slice message);
+
+ friend td::StringBuilder &operator<<(td::StringBuilder &sb, const CallVerification &verification);
+
+ private:
+ td::int64 user_id_{};
+ PrivateKey private_key_;
+ CallVerificationChain chain_;
+ std::vector<tde2e_api::Bytes> pending_outbound_messages_;
+ bool sent_commit_{false};
+ bool sent_reveal_{false};
+
+ td::int32 height_{-1};
+ td::UInt256 last_block_hash_{};
+ td::UInt256 nonce_{};
+};
+
+struct Call {
+ static td::Result<std::string> create_zero_block(const PrivateKey &private_key, GroupStateRef group_state);
+ static td::Result<std::string> create_self_add_block(const PrivateKey &private_key, td::Slice previous_block,
+ const GroupParticipant &self);
+
+ static td::Result<Call> create(td::int64 user_id, PrivateKey private_key, td::Slice last_block);
+ static td::Result<std::vector<Change>> make_changes_for_new_state(GroupStateRef group_state);
+
+ td::Result<std::string> build_change_state(GroupStateRef new_group_state) const;
+ td::Result<td::int32> get_height() const;
+ td::Result<GroupStateRef> get_group_state() const;
+
+ td::Status apply_block(td::Slice block);
+
+ td::Status get_status() const {
+ if (status_.is_error()) {
+ return Error(E::CallFailed, PSLICE() << status_);
+ }
+ return td::Status::OK();
+ }
+
+ td::Result<td::SecureString> shared_key() const {
+ TRY_STATUS(get_status());
+ return group_shared_key_.copy();
+ }
+
+ td::Result<std::string> decrypt(td::int64 user_id, td::int32 channel_id, td::Slice encrypted_data) {
+ TRY_STATUS(get_status());
+ return call_encryption_.decrypt(user_id, channel_id, encrypted_data);
+ }
+ td::Result<std::string> encrypt(td::int32 channel_id, td::Slice decrypted_data) {
+ TRY_STATUS(get_status());
+ return call_encryption_.encrypt(channel_id, decrypted_data);
+ }
+
+ td::Result<std::vector<std::string>> pull_outbound_messages() {
+ TRY_STATUS(get_status());
+ return call_verification_.pull_outbound_messages();
+ }
+
+ td::Result<CallVerificationState> get_verification_state() const {
+ TRY_STATUS(get_status());
+ return call_verification_.get_verification_state();
+ }
+ td::Result<CallVerificationWords> get_verification_words() const {
+ TRY_STATUS(get_status());
+ return call_verification_.get_verification_words();
+ }
+ td::Result<CallVerificationState> receive_inbound_message(td::Slice verification_message) {
+ TRY_STATUS(get_status());
+ // For now, don't fail the call in case of some errors
+ TRY_RESULT(local_verification_message, Blockchain::from_server_to_local(verification_message.str()));
+ TRY_STATUS(call_verification_.receive_inbound_message(local_verification_message));
+ return get_verification_state();
+ }
+ friend td::StringBuilder &operator<<(td::StringBuilder &sb, const Call &call);
+
+ private:
+ td::Status status_{td::Status::OK()};
+ td::int64 user_id_{0};
+ PrivateKey private_key_;
+ ClientBlockchain blockchain_;
+ CallVerification call_verification_;
+ CallEncryption call_encryption_;
+ td::SecureString group_shared_key_;
+
+ Call(td::int64 user_id, PrivateKey pk, ClientBlockchain blockchain);
+
+ td::Status update_group_shared_key();
+ td::Status do_apply_block(td::Slice block);
+ td::Result<td::SecureString> decrypt_shared_key();
+};
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/CheckSharedSecret.cpp b/tde2e/td/e2e/CheckSharedSecret.cpp
new file mode 100644
index 000000000..032237419
--- /dev/null
+++ b/tde2e/td/e2e/CheckSharedSecret.cpp
@@ -0,0 +1,75 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/e2e/CheckSharedSecret.h"
+
+#include "td/utils/crypto.h"
+
+#include <utility>
+
+namespace tde2e_core {
+
+CheckSharedSecret CheckSharedSecret::create() {
+ CheckSharedSecret result;
+ result.nonce_ = generate_nonce();
+ td::sha256(result.nonce_.as_slice(), result.nonce_hash_.as_mutable_slice());
+ return result;
+}
+
+td::UInt256 CheckSharedSecret::commit_nonce() const {
+ return nonce_hash_;
+}
+
+td::Result<td::UInt256> CheckSharedSecret::reveal_nonce() const {
+ if (!o_other_nonce_hash_) {
+ return td::Status::Error("Cannot reveal nonce before other nonce hash is known");
+ }
+ return nonce_;
+}
+
+td::Status CheckSharedSecret::recive_commit_nonce(const td::UInt256 &other_nonce_hash) {
+ if (o_other_nonce_hash_) {
+ return td::Status::Error("Already received other nonce hash");
+ }
+ o_other_nonce_hash_ = other_nonce_hash;
+ return td::Status::OK();
+}
+
+td::Status CheckSharedSecret::receive_reveal_nonce(const td::UInt256 &other_nonce) {
+ if (!o_other_nonce_hash_) {
+ return td::Status::Error("Cannot receive nonce before nonce hash");
+ }
+ td::UInt256 expected_nonce_hash;
+ td::sha256(other_nonce.as_slice(), expected_nonce_hash.as_mutable_slice());
+ if (expected_nonce_hash != *o_other_nonce_hash_) {
+ return td::Status::Error("Other nonce hash is different from the expected one");
+ }
+ o_other_nonce_ = other_nonce;
+ return td::Status::OK();
+}
+
+td::Result<td::UInt256> CheckSharedSecret::finalize_hash(td::Slice shared_secret) const {
+ if (!o_other_nonce_) {
+ return td::Status::Error("Cannot calculate hash before other nonce is known");
+ }
+ td::UInt256 a = nonce_;
+ td::UInt256 b = *o_other_nonce_;
+ if (b < a) {
+ std::swap(a, b);
+ }
+ td::Sha256State state;
+ state.init();
+ state.feed(shared_secret);
+ state.feed(a.as_slice());
+ state.feed(b.as_slice());
+
+ td::UInt256 hash;
+ state.extract(hash.as_mutable_slice());
+
+ return hash;
+}
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/CheckSharedSecret.h b/tde2e/td/e2e/CheckSharedSecret.h
new file mode 100644
index 000000000..2d7c71d8a
--- /dev/null
+++ b/tde2e/td/e2e/CheckSharedSecret.h
@@ -0,0 +1,32 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include "td/e2e/utils.h"
+
+#include "td/utils/optional.h"
+#include "td/utils/Slice.h"
+#include "td/utils/Status.h"
+#include "td/utils/UInt.h"
+
+namespace tde2e_core {
+
+struct CheckSharedSecret {
+ td::UInt256 nonce_;
+ td::UInt256 nonce_hash_;
+ td::optional<td::UInt256> o_other_nonce_hash_;
+ td::optional<td::UInt256> o_other_nonce_;
+
+ static CheckSharedSecret create();
+ td::UInt256 commit_nonce() const;
+ td::Result<td::UInt256> reveal_nonce() const;
+ td::Status recive_commit_nonce(const td::UInt256 &other_nonce_hash);
+ td::Status receive_reveal_nonce(const td::UInt256 &other_nonce);
+ td::Result<td::UInt256> finalize_hash(td::Slice shared_secret) const;
+};
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/Container.h b/tde2e/td/e2e/Container.h
new file mode 100644
index 000000000..06b0239e6
--- /dev/null
+++ b/tde2e/td/e2e/Container.h
@@ -0,0 +1,256 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include "td/e2e/e2e_errors.h"
+#include "td/e2e/utils.h"
+
+#include "td/utils/common.h"
+#include "td/utils/FlatHashMap.h"
+#ifndef ENGINE
+#include "td/utils/SliceBuilder.h"
+#endif
+#include "td/utils/Status.h"
+#include "td/utils/UInt.h"
+
+#include <atomic>
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <tuple>
+#include <type_traits>
+#include <utility>
+
+namespace tde2e_core {
+
+template <typename T, bool IsMutable, bool HasHash>
+struct TypeInfo {
+ using type = T;
+ static constexpr bool is_mutable = IsMutable;
+ static constexpr bool has_hash = HasHash;
+};
+
+template <typename T, typename...>
+struct TypeIndex;
+template <typename T, typename First, typename... Rest>
+struct TypeIndex<T, First, Rest...> {
+ static constexpr size_t value = std::is_same<T, typename First::type>::value ? 0 : 1 + TypeIndex<T, Rest...>::value;
+};
+template <typename T>
+struct TypeIndex<T> {
+ static constexpr size_t value = 0;
+};
+template <typename T, typename...>
+struct TypeInfoFor;
+template <typename T, typename First, typename... Rest>
+struct TypeInfoFor<T, First, Rest...> {
+ using type =
+ std::conditional_t<std::is_same<T, typename First::type>::value, First, typename TypeInfoFor<T, Rest...>::type>;
+};
+template <typename T>
+struct TypeInfoFor<T> {
+ using type = void;
+};
+
+template <typename T>
+struct MutableValue {
+ MutableValue(T value) : value(std::move(value)) {
+ }
+ T value;
+ mutable std::mutex mutex;
+};
+
+struct MutexUnlockDeleter {
+ std::shared_ptr<void> value_ptr;
+ std::unique_lock<std::mutex> lock;
+
+ MutexUnlockDeleter(MutexUnlockDeleter &&other) : value_ptr(std::move(other.value_ptr)), lock(std::move(other.lock)) {
+ }
+ template <typename T>
+ MutexUnlockDeleter(std::shared_ptr<MutableValue<T>> ptr, std::unique_lock<std::mutex> &&l)
+ : value_ptr(std::move(ptr)), lock(std::move(l)) {
+ }
+
+ template <typename T>
+ void operator()(T * /*unused*/) {
+ }
+};
+
+template <typename TypeInfo>
+struct TypeStorage {
+ using T = typename TypeInfo::type;
+ using ValueType = std::conditional_t<TypeInfo::is_mutable, MutableValue<T>, T>;
+ using ValueRef = std::shared_ptr<ValueType>;
+
+ struct Entry {
+ Entry(std::optional<td::UInt256> o_hash, ValueRef value) : o_hash(std::move(o_hash)), value(std::move(value)) {
+ }
+ std::optional<td::UInt256> o_hash;
+ ValueRef value;
+ };
+
+ td::FlatHashMap<td::int64, Entry> values;
+ td::FlatHashMap<td::UInt256, td::int64, UInt256Hash> hash_to_id;
+ mutable std::mutex map_mutex;
+};
+
+template <typename T>
+using SharedRef = std::shared_ptr<const T>;
+template <typename T>
+using UniqueRef = std::unique_ptr<T, MutexUnlockDeleter>;
+
+template <typename... TypeInfos>
+class Container {
+ using StoragesTuple = std::tuple<TypeStorage<TypeInfos>...>;
+ StoragesTuple storages_;
+ std::atomic<td::int64> next_id{1};
+
+ template <typename T>
+ auto &get_storage() {
+ constexpr size_t index = TypeIndex<T, TypeInfos...>::value;
+ return std::get<index>(storages_);
+ }
+
+ template <typename T>
+ const auto &get_storage() const {
+ constexpr size_t index = TypeIndex<T, TypeInfos...>::value;
+ return std::get<index>(storages_);
+ }
+
+ public:
+ using Id = td::int64;
+
+ template <typename T, typename... Args>
+ Id emplace(Args &&...args) {
+ return try_build<T>({}, [&]() -> td::Result<T> { return T(std::forward<Args>(args)...); }).move_as_ok();
+ }
+ template <typename T, typename... Args>
+ Id try_emplace(td::UInt256 hash, Args &&...args) {
+ return try_build<T>(hash, [&]() -> td::Result<T> { return T(std::forward<Args>(args)...); }).move_as_ok();
+ }
+
+ template <typename T, typename F>
+ td::Result<Id> try_build(std::optional<td::UInt256> o_hash, F &&f) {
+ using TI = typename TypeInfoFor<T, TypeInfos...>::type;
+ auto &storage = get_storage<T>();
+
+ if constexpr (TI::has_hash) {
+ if (o_hash) {
+ std::unique_lock map_lock(storage.map_mutex);
+ auto it = storage.hash_to_id.find(*o_hash);
+ if (it != storage.hash_to_id.end()) {
+ return it->second;
+ }
+ }
+ } else {
+ CHECK(!o_hash);
+ }
+
+ TRY_RESULT(value, f());
+
+ std::unique_lock map_lock(storage.map_mutex);
+ if constexpr (TI::has_hash) {
+ if (o_hash) {
+ auto it = storage.hash_to_id.find(*o_hash);
+ if (it != storage.hash_to_id.end()) {
+ return it->second;
+ }
+ }
+ }
+
+ auto id = next_id.fetch_add(1, std::memory_order_relaxed);
+ if constexpr (TI::is_mutable) {
+ auto value_ptr = std::make_shared<MutableValue<T>>(std::move(value));
+ storage.values.emplace(id, std::move(o_hash), value_ptr);
+ } else {
+ auto value_ptr = std::make_shared<T>(std::move(value));
+ storage.values.emplace(id, std::move(o_hash), value_ptr);
+ }
+
+ if constexpr (TI::has_hash) {
+ if (o_hash) {
+ storage.hash_to_id.emplace(*o_hash, id);
+ }
+ }
+ return id;
+ }
+
+ template <typename T>
+ td::Result<SharedRef<T>> get_shared(Id id) const {
+ using TI = typename TypeInfoFor<T, TypeInfos...>::type;
+ static_assert(!TI::is_mutable, "Use get_mutable for mutable types");
+ const auto &storage = get_storage<T>();
+
+ std::unique_lock map_lock(storage.map_mutex);
+ auto it = storage.values.find(id);
+ if (it == storage.values.end()) {
+ return td::Status::Error(static_cast<int>(tde2e_api::ErrorCode::InvalidId), PSLICE()
+ << "Invalid identifier = " << id);
+ }
+ return it->second.value;
+ }
+
+ template <typename T>
+ td::Result<UniqueRef<T>> get_unique(Id id) {
+ using TI = typename TypeInfoFor<T, TypeInfos...>::type;
+ static_assert(TI::is_mutable, "Use get_shared for immutable types");
+ auto &storage = get_storage<T>();
+
+ std::unique_lock map_lock(storage.map_mutex);
+ auto it = storage.values.find(id);
+ if (it == storage.values.end()) {
+ return td::Status::Error(static_cast<int>(tde2e_api::ErrorCode::InvalidId), PSLICE()
+ << "Invalid identifier = " << id);
+ }
+
+ auto value_ref = it->second.value;
+ std::unique_lock value_lock(value_ref->mutex);
+ auto value_ptr = &value_ref->value;
+
+ return std::unique_ptr<T, MutexUnlockDeleter>(value_ptr,
+ MutexUnlockDeleter(std::move(value_ref), std::move(value_lock)));
+ }
+
+ template <typename T>
+ td::Status destroy(std::optional<Id> o_id) {
+ auto &storage = get_storage<T>();
+ std::scoped_lock<std::mutex> lock(storage.map_mutex);
+ if (o_id) {
+ auto it = storage.values.find(*o_id);
+ if (it == storage.values.end()) {
+ return td::Status::Error(static_cast<int>(tde2e_api::ErrorCode::InvalidInput), "Unknown key identifier");
+ }
+ if (it->second.o_hash) {
+ storage.hash_to_id.erase(*it->second.o_hash);
+ }
+ storage.values.erase(it);
+ return td::Status::OK();
+ }
+ storage.hash_to_id.clear();
+ storage.values.clear();
+ return td::Status::OK();
+ }
+};
+
+template <class T, class S>
+td::Result<SharedRef<T>> convert(SharedRef<S> from) {
+ if (std::holds_alternative<T>(*from)) {
+ return SharedRef<T>(from, &std::get<T>(*from));
+ }
+ return td::Status::Error(static_cast<int>(tde2e_api::ErrorCode::UnknownError), "TODO");
+}
+
+template <class T, class S>
+td::Result<UniqueRef<T>> convert(UniqueRef<S> from) {
+ if (std::holds_alternative<T>(*from)) {
+ auto value_ptr = &std::get<T>(*from);
+ return UniqueRef<T>(value_ptr, std::move(from.get_deleter()));
+ }
+ return td::Status::Error(static_cast<int>(tde2e_api::ErrorCode::UnknownError), "TODO");
+}
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/DecryptedKey.cpp b/tde2e/td/e2e/DecryptedKey.cpp
new file mode 100644
index 000000000..0cfe7e1d4
--- /dev/null
+++ b/tde2e/td/e2e/DecryptedKey.cpp
@@ -0,0 +1,40 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/e2e/DecryptedKey.h"
+
+#include "td/e2e/EncryptedKey.h"
+#include "td/e2e/MessageEncryption.h"
+
+#include "td/utils/algorithm.h"
+
+namespace tde2e_core {
+
+DecryptedKey::DecryptedKey(const Mnemonic &mnemonic)
+ : mnemonic_words(mnemonic.get_words()), private_key(mnemonic.to_private_key()) {
+}
+DecryptedKey::DecryptedKey(std::vector<td::SecureString> mnemonic_words, PrivateKey key)
+ : mnemonic_words(std::move(mnemonic_words)), private_key(std::move(key)) {
+}
+DecryptedKey::DecryptedKey(RawDecryptedKey key)
+ : DecryptedKey(std::move(key.mnemonic_words), PrivateKey::from_slice(key.private_key).move_as_ok()) {
+}
+
+EncryptedKey DecryptedKey::encrypt(td::Slice local_password, td::Slice secret) const {
+ td::SecureString decrypted_secret = MessageEncryption::hmac_sha512(secret, local_password);
+
+ td::SecureString encryption_secret =
+ MessageEncryption::kdf(as_slice(decrypted_secret), "tde2e local key", EncryptedKey::PBKDF_ITERATIONS);
+
+ std::vector<td::SecureString> mnemonic_words_copy =
+ td::transform(mnemonic_words, [](const auto &word) { return word.copy(); });
+ auto data = td::serialize_secure(RawDecryptedKey{std::move(mnemonic_words_copy), private_key.to_secure_string()});
+ auto encrypted_data = MessageEncryption::encrypt_data(data, as_slice(encryption_secret));
+
+ return EncryptedKey{std::move(encrypted_data), private_key.to_public_key(), {}};
+}
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/DecryptedKey.h b/tde2e/td/e2e/DecryptedKey.h
new file mode 100644
index 000000000..27193bbd1
--- /dev/null
+++ b/tde2e/td/e2e/DecryptedKey.h
@@ -0,0 +1,52 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include "td/e2e/Mnemonic.h"
+
+#include "td/utils/SharedSlice.h"
+#include "td/utils/Slice.h"
+#include "td/utils/tl_helpers.h"
+
+#include <string>
+#include <vector>
+
+namespace tde2e_core {
+
+struct RawDecryptedKey {
+ std::vector<td::SecureString> mnemonic_words;
+ td::SecureString private_key;
+
+ template <class StorerT>
+ void store(StorerT &storer) const {
+ using td::store;
+ store(mnemonic_words, storer);
+ store(private_key, storer);
+ }
+
+ template <class ParserT>
+ void parse(ParserT &parser) {
+ using td::parse;
+ parse(mnemonic_words, parser);
+ parse(private_key, parser);
+ }
+};
+
+struct EncryptedKey;
+struct DecryptedKey {
+ DecryptedKey() = delete;
+ explicit DecryptedKey(const Mnemonic &mnemonic);
+ DecryptedKey(std::vector<td::SecureString> mnemonic_words, PrivateKey key);
+ explicit DecryptedKey(RawDecryptedKey key);
+
+ std::vector<td::SecureString> mnemonic_words;
+ PrivateKey private_key;
+
+ EncryptedKey encrypt(td::Slice local_password, td::Slice secret = {}) const;
+};
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/EncryptedKey.cpp b/tde2e/td/e2e/EncryptedKey.cpp
new file mode 100644
index 000000000..5b8894be7
--- /dev/null
+++ b/tde2e/td/e2e/EncryptedKey.cpp
@@ -0,0 +1,38 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/e2e/EncryptedKey.h"
+
+#include "td/e2e/DecryptedKey.h"
+#include "td/e2e/MessageEncryption.h"
+
+#include "td/utils/tl_helpers.h"
+
+namespace tde2e_core {
+
+td::Result<DecryptedKey> EncryptedKey::decrypt(td::Slice local_password, bool check_public_key) const {
+ /*
+ if (secret.size() != 32) {
+ return td::Status::Error("Failed to decrypt key: invalid secret size");
+ }
+ */
+ auto decrypted_secret = MessageEncryption::hmac_sha512(secret, local_password);
+
+ td::SecureString encryption_secret =
+ MessageEncryption::kdf(as_slice(decrypted_secret), "tde2e local key", EncryptedKey::PBKDF_ITERATIONS);
+
+ TRY_RESULT(decrypted_data, MessageEncryption::decrypt_data(as_slice(encrypted_data), as_slice(encryption_secret)));
+
+ RawDecryptedKey raw_decrypted_key;
+ TRY_STATUS(td::unserialize(raw_decrypted_key, decrypted_data));
+ DecryptedKey res(std::move(raw_decrypted_key));
+ if (check_public_key && !(res.private_key.to_public_key() == this->o_public_key)) {
+ return td::Status::Error("Something wrong: public key of decrypted private key differs from requested public key");
+ }
+ return std::move(res);
+}
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/EncryptedKey.h b/tde2e/td/e2e/EncryptedKey.h
new file mode 100644
index 000000000..37eb887f0
--- /dev/null
+++ b/tde2e/td/e2e/EncryptedKey.h
@@ -0,0 +1,29 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include "td/e2e/utils.h"
+
+#include "td/utils/optional.h"
+#include "td/utils/SharedSlice.h"
+#include "td/utils/Slice.h"
+#include "td/utils/Status.h"
+
+namespace tde2e_core {
+
+struct DecryptedKey;
+struct EncryptedKey {
+ static constexpr int PBKDF_ITERATIONS = 100000;
+ static constexpr int PBKDF_FAST_ITERATIONS = 1;
+ td::SecureString encrypted_data;
+ td::optional<PublicKey> o_public_key;
+ td::SecureString secret;
+
+ td::Result<DecryptedKey> decrypt(td::Slice local_password, bool check_public_key = true) const;
+};
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/EncryptedStorage.cpp b/tde2e/td/e2e/EncryptedStorage.cpp
new file mode 100644
index 000000000..359e5eeb7
--- /dev/null
+++ b/tde2e/td/e2e/EncryptedStorage.cpp
@@ -0,0 +1,369 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/e2e/EncryptedStorage.h"
+
+#include "td/e2e/Blockchain.h"
+
+#include "td/telegram/e2e_api.hpp"
+
+#include "td/utils/crypto.h"
+#include "td/utils/logging.h"
+#include "td/utils/overloaded.h"
+#include "td/utils/tl_parsers.h"
+
+namespace tde2e_core {
+
+api::UserId from_tl(td::int64 user_id) {
+ return user_id;
+}
+
+api::Name from_tl(td::e2e_api::e2e_personalName &name) {
+ return api::Name{name.first_name_, name.last_name_};
+}
+
+api::UserId from_tl(td::e2e_api::e2e_personalUserId &user_id) {
+ return user_id.user_id_;
+}
+
+api::PhoneNumber from_tl(td::e2e_api::e2e_personalPhoneNumber &phone_number) {
+ return api::PhoneNumber{phone_number.phone_number_};
+}
+
+api::EmojiNonces from_tl(td::e2e_api::e2e_personalEmojiNonces &emoji_nonces) {
+ using Flags = td::e2e_api::e2e_personalEmojiNonces;
+ api::EmojiNonces res;
+ if ((emoji_nonces.flags_ & Flags::SELF_NONCE_MASK) != 0) {
+ res.self_nonce = from_td(emoji_nonces.self_nonce_);
+ }
+ return res;
+}
+
+api::ContactState from_tl(td::e2e_api::e2e_personalContactState &contact_state) {
+ return api::ContactState{contact_state.is_contact_ ? api::ContactState::Contact : api::ContactState::NotContact};
+}
+
+void init(api::Contact &contact, api::Entry<api::UserId> user_id) {
+ contact.o_user_id = user_id;
+}
+
+void init(api::Contact &contact, api::Entry<api::Name> name) {
+ contact.o_name = std::move(name);
+}
+
+void init(api::Contact &contact, api::Entry<api::PhoneNumber> phone_number) {
+ contact.o_phone_number = std::move(phone_number);
+}
+
+void init(api::Contact &contact, api::Entry<api::EmojiNonces> emoji_nonces) {
+ contact.emoji_nonces = std::move(emoji_nonces);
+}
+
+void init(api::Contact &contact, api::Entry<api::ContactState> contact_state) {
+ contact.contact_state = std::move(contact_state);
+}
+
+api::Contact from_tl(td::e2e_api::e2e_valueContactByPublicKey &value) {
+ api::Contact contact;
+ for (auto &entry : value.entries_) {
+ td::e2e_api::downcast_call(*entry->personal_, [&](auto &tl_value) {
+ auto entry_value = from_tl(tl_value);
+ using ValueT = decltype(entry_value);
+ init(contact, api::Entry<ValueT>{api::Entry<ValueT>::Self, static_cast<td::uint32>(entry->signed_at_),
+ std::move(entry_value)});
+ });
+ }
+ return contact;
+}
+
+bool reduce(api::Entry<api::EmojiNonces> &a, const api::Entry<api::EmojiNonces> &b) {
+ // do not care about timestamp, first write always win
+ // TODO: handle source
+ auto &nonces = a.value;
+ const auto &other_nonces = b.value;
+
+ bool changed = false;
+
+ if (!nonces.self_nonce && other_nonces.self_nonce) {
+ nonces.self_nonce = other_nonces.self_nonce;
+ changed = true;
+ }
+ if (!nonces.contact_nonce_hash && other_nonces.contact_nonce_hash) {
+ nonces.contact_nonce_hash = other_nonces.contact_nonce_hash;
+ changed = true;
+ }
+ if (!nonces.contact_nonce && other_nonces.contact_nonce) {
+ nonces.contact_nonce = other_nonces.contact_nonce;
+ changed = true;
+ }
+ return changed;
+}
+
+template <class T>
+bool reduce(api::Entry<T> &a, const api::Entry<T> &b) {
+ if (a.timestamp > b.timestamp) {
+ a = std::move(b);
+ return false;
+ }
+ // TODO: handle source
+ return false;
+}
+
+template <class T>
+bool reduce(api::Entry<T> &a, const std::optional<api::Entry<T>> &b) {
+ if (!b) {
+ return false;
+ }
+ return reduce(a, *b);
+}
+
+template <class T>
+bool reduce(std::optional<api::Entry<T>> &a, const std::optional<api::Entry<T>> &b) {
+ if (!a) {
+ a = b;
+ return static_cast<bool>(b);
+ }
+ if (!b) {
+ return false;
+ }
+ return reduce(*a, *b);
+}
+
+bool reduce(Update &a, const Update &b) {
+ bool changed = false;
+ changed = reduce(a.o_user_id, b.o_user_id);
+ changed = reduce(a.o_name, b.o_name);
+ changed = reduce(a.o_phone_number, b.o_phone_number);
+ changed = reduce(a.o_emoji_nonces, b.o_emoji_nonces);
+ changed = reduce(a.o_contact_state, b.o_contact_state);
+ return changed;
+}
+
+std::optional<Value> apply_update(const std::optional<Value> &o_value, const Update &update) {
+ auto value = o_value.value_or(Value());
+ bool changed = false;
+ changed |= reduce(value.o_name, update.o_name);
+ changed |= reduce(value.o_phone_number, update.o_phone_number);
+ changed |= reduce(value.o_user_id, update.o_user_id);
+ changed |= reduce(value.emoji_nonces, update.o_emoji_nonces);
+ changed |= reduce(value.contact_state, update.o_contact_state);
+ if (changed) {
+ return value;
+ }
+ return std::nullopt;
+}
+
+td::Status validate(const api::EmojiNonces &nonces) {
+ if (nonces.contact_nonce && !nonces.self_nonce) {
+ return td::Status::Error("Receive contact_nonce BEFORE self_nonce");
+ }
+ if (nonces.contact_nonce && !nonces.contact_nonce_hash) {
+ return td::Status::Error("Receive contact_nonce BEFORE concat_nonce_hash");
+ }
+ if (nonces.contact_nonce) {
+ auto &contact_nonce = nonces.contact_nonce.value();
+ api::Int256 contact_nonce_hash;
+ td::sha256(td::Slice(contact_nonce.data(), contact_nonce.size()),
+ td::MutableSlice(contact_nonce_hash.data(), contact_nonce_hash.size()));
+
+ if (contact_nonce_hash != nonces.contact_nonce_hash.value()) {
+ return td::Status::Error("Invalid concat_nonce (hash mismatch)");
+ }
+ }
+ return td::Status::OK();
+}
+
+td::Result<EncryptedStorage::UpdateId> EncryptedStorage::update(Key key, Update update) {
+ LOG(INFO) << "Update [receive] " << key << " " << update;
+
+ auto update_id = ++next_update_id_;
+ auto it = updates_.find(key);
+ if (it == updates_.end()) {
+ // create pending update (original value is unknown)
+ updates_.emplace(key, UpdateInfo{{update_id}, std::move(update), {}});
+ LOG(INFO) << "Update [delay] " << key << " " << update;
+ return update_id;
+ }
+
+ auto &update_info = it->second;
+ reduce(update_info.update, update);
+ update_info.update_ids.emplace_back(update_id);
+ LOG(INFO) << "Update [reduce] " << key << " " << update_info.update;
+
+ if (update_info.o_new_value && !reapply_update(update_info, std::move(update_info.o_new_value))) {
+ LOG(INFO) << "Update [drop] " << key << " " << update;
+ updates_.erase(it);
+ }
+ return update_id;
+}
+
+td::Result<std::optional<Value>> EncryptedStorage::get(Key key, bool optimistic) {
+ auto it = partial_key_value_.find(key);
+ if (it != partial_key_value_.end()) {
+ if (optimistic) {
+ auto update_it = updates_.find(key);
+ if (update_it != updates_.end()) {
+ CHECK(update_it->second.o_new_value);
+ return *update_it->second.o_new_value;
+ }
+ }
+ return it->second;
+ }
+ return td::Status::Error("NEED_PROOF");
+}
+
+td::int64 EncryptedStorage::get_height() const {
+ return blockchain_.get_height();
+}
+
+td::Result<EncryptedStorage::KeyValueUpdates> EncryptedStorage::apply_block(td::Slice block) {
+ TRY_RESULT(changes, blockchain_.try_apply_block(block));
+ KeyValueUpdates updates;
+ for (auto &change : changes) {
+ bool skip = false;
+ td::Result<std::pair<Key, std::optional<Value>>> r_p;
+ std::visit(td::overloaded([&](ChangeNoop &noop) {},
+ [&](ChangeSetValue &set_value) { r_p = parse(set_value.key, set_value.value); },
+ [&](ChangeSetGroupState &) { skip = true; }, [&](ChangeSetSharedKey &) { skip = true; }),
+ change.value);
+ if (skip) {
+ continue;
+ }
+ if (r_p.is_error()) {
+ LOG(ERROR) << "BUG! change from blockchain is ignored: " << r_p.error();
+ continue;
+ }
+ auto p = r_p.move_as_ok();
+ updates.updates.emplace_back(p.first, p.second);
+ sync_entry(std::move(p.first), std::move(p.second), true);
+ }
+ return updates;
+}
+
+td::Status EncryptedStorage::add_proof(td::Slice proof, td::Span<std::string> keys) {
+ TRY_STATUS(blockchain_.add_proof(proof));
+ // sync keys
+ for (const auto &key : keys) {
+ auto r_value = blockchain_.get_value(key);
+ if (r_value.is_error()) {
+ LOG(ERROR) << "Failed to get value from proof " << r_value.error();
+ continue;
+ }
+
+ auto raw_value = r_value.move_as_ok();
+ auto r_p = parse(key, raw_value);
+ if (r_p.is_error()) {
+ LOG(ERROR) << "BUG! value from blockchain is ignored: " << r_p.error();
+ continue;
+ }
+
+ auto p = r_p.move_as_ok();
+ sync_entry(std::move(p.first), std::move(p.second));
+ }
+
+ return td::Status::OK();
+}
+
+EncryptedStorage::BlockchainState EncryptedStorage::get_blockchain_state() {
+ //TODO add indexes
+
+ // check if some values are unknown
+ BlockchainState state;
+ std::vector<Change> changes;
+ for (auto &update : updates_) {
+ if (!update.second.o_new_value) {
+ state.need_proofs.emplace_back(encrypt_key(update.first));
+ } else {
+ changes.emplace_back(
+ Change{ChangeSetValue{encrypt_key(update.first), encrypt_value(update.second.o_new_value.value())}});
+ }
+ }
+ if (!changes.empty()) {
+ state.next_block = blockchain_.build_block(changes, private_key_).move_as_ok();
+ }
+ return state;
+}
+
+EncryptedStorage::KeyValueUpdates EncryptedStorage::pull_updates() {
+ return std::move(pending_key_value_updates_);
+}
+
+td::Result<std::pair<Key, std::optional<Value>>> EncryptedStorage::parse(td::Slice raw_key, td::Slice raw_value) {
+ TRY_RESULT(key, decrypt_key(raw_key));
+ TRY_RESULT(value, decrypt_value(raw_value));
+ return std::make_pair(std::move(key), std::move(value));
+}
+
+void EncryptedStorage::sync_entry(Key key, std::optional<Value> value, bool rewrite) {
+ LOG(INFO) << "Sync [new] " << key << " " << value;
+ auto p = partial_key_value_.try_emplace(key, std::move(value));
+ if (!p.second) {
+ if (rewrite) {
+ p.first->second = std::move(value);
+ } else {
+ // CHECK(p.first->second == value);
+ }
+ }
+
+ if (p.second || rewrite) {
+ auto it = updates_.find(key);
+ if (it != updates_.end()) {
+ auto &update_info = it->second;
+ if (!reapply_update(update_info, std::move(value))) {
+ LOG(INFO) << "Update [drop] " << key << " " << update_info.update;
+ updates_.erase(it);
+ }
+ }
+ }
+}
+
+bool EncryptedStorage::reapply_update(UpdateInfo &update_info, const std::optional<Value> &value) {
+ auto o_new_value = apply_update(value, update_info.update);
+ if (o_new_value) {
+ update_info.o_new_value = std::move(o_new_value);
+ LOG(INFO) << "Update [reapply] value=" << update_info.o_new_value;
+ return true;
+ }
+
+ // TODO: complete updates
+ return false;
+}
+
+std::string EncryptedStorage::encrypt_key(const Key &key) const {
+ td::string res(32, '\0');
+ auto iv = secret_for_key_.as_slice().substr(32, 32).str();
+ td::aes_cbc_encrypt(secret_for_key_.as_slice().substr(0, 32), iv, key.public_key.as_slice(), res);
+ return res;
+}
+
+td::Result<Key> EncryptedStorage::decrypt_key(td::Slice raw_key) const {
+ if (raw_key.size() != 32) {
+ return td::Status::Error("Invalid key length");
+ }
+ td::UInt256 key;
+ auto iv = secret_for_key_.as_slice().substr(32, 32).str();
+ td::aes_cbc_decrypt(secret_for_key_.as_slice().substr(0, 32), iv, raw_key, key.as_mutable_slice());
+ return Key{key};
+}
+
+std::string EncryptedStorage::encrypt_value(const Value &value) const {
+ return MessageEncryption::encrypt_data(serialize_boxed(*to_tl(value)), secret_for_value_).as_slice().str();
+}
+
+td::Result<std::optional<Value>> EncryptedStorage::decrypt_value(td::Slice raw_value) const {
+ if (raw_value.empty()) {
+ return std::nullopt;
+ }
+ TRY_RESULT(decrypted_raw_value, MessageEncryption::decrypt_data(raw_value, secret_for_value_));
+ td::TlParser parser(decrypted_raw_value);
+ auto value_tl =
+ td::e2e_api::move_object_as<td::e2e_api::e2e_valueContactByPublicKey>(td::e2e_api::e2e_Value::fetch(parser));
+ parser.fetch_end();
+ TRY_STATUS(parser.get_status());
+ return from_tl(*value_tl);
+}
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/EncryptedStorage.h b/tde2e/td/e2e/EncryptedStorage.h
new file mode 100644
index 000000000..b4781e62f
--- /dev/null
+++ b/tde2e/td/e2e/EncryptedStorage.h
@@ -0,0 +1,410 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include "td/e2e/Blockchain.h"
+#include "td/e2e/Container.h"
+#include "td/e2e/e2e_api.h"
+#include "td/e2e/MessageEncryption.h"
+#include "td/e2e/utils.h"
+
+#include "td/telegram/e2e_api.h"
+
+#include "td/utils/base64.h"
+#include "td/utils/common.h"
+#include "td/utils/SharedSlice.h"
+#include "td/utils/Slice.h"
+#include "td/utils/Span.h"
+#include "td/utils/Status.h"
+#include "td/utils/StringBuilder.h"
+#include "td/utils/UInt.h"
+
+#include <map>
+#include <optional>
+#include <utility>
+
+namespace td {
+
+template <class T>
+StringBuilder &operator<<(td::StringBuilder &sb, std::optional<T> const &opt) {
+ if (opt.has_value()) {
+ sb << "Some{" << opt.value() << "}";
+ } else {
+ sb << "None";
+ }
+ return sb;
+}
+
+} // namespace td
+
+namespace tde2e_core {
+namespace api = tde2e_api;
+struct Update {
+ std::optional<api::Entry<api::UserId>> o_user_id;
+ std::optional<api::Entry<api::Name>> o_name;
+ std::optional<api::Entry<api::PhoneNumber>> o_phone_number;
+
+ std::optional<api::Entry<api::EmojiNonces>> o_emoji_nonces;
+ std::optional<api::Entry<api::ContactState>> o_contact_state;
+};
+
+inline api::Int256 from_td(const td::UInt256 &value) {
+ api::Int256 result;
+ td::MutableSlice(result.data(), result.size()).copy_from(value.as_slice());
+ return result;
+}
+
+inline api::Int512 from_td(const td::UInt512 &value) {
+ api::Int512 result;
+ td::MutableSlice(result.data(), result.size()).copy_from(value.as_slice());
+ return result;
+}
+} // namespace tde2e_core
+
+namespace tde2e_api {
+using Update = tde2e_core::Update;
+
+inline td::UInt256 to_td(const Int256 &value) {
+ td::UInt256 result;
+ result.as_mutable_slice().copy_from(td::Slice(value.data(), value.size()));
+ return result;
+}
+inline td::UInt512 to_td(const Int512 &value) {
+ td::UInt512 result;
+ result.as_mutable_slice().copy_from(td::Slice(value.data(), value.size()));
+ return result;
+}
+inline auto to_tl(const UserId &entry) {
+ return td::e2e_api::make_object<td::e2e_api::e2e_personalUserId>(entry);
+}
+inline auto to_tl(const Name &entry) {
+ return td::e2e_api::make_object<td::e2e_api::e2e_personalName>(entry.first_name, entry.last_name);
+}
+inline auto to_tl(const PhoneNumber &entry) {
+ return td::e2e_api::make_object<td::e2e_api::e2e_personalPhoneNumber>(entry.phone_number);
+}
+
+inline auto to_tl(const EmojiNonces &entry) {
+ using TlType = td::e2e_api::e2e_personalEmojiNonces;
+ td::int32 flags = TlType::SELF_NONCE_MASK * static_cast<bool>(entry.self_nonce) +
+ TlType::CONTACT_NONCE_MASK * static_cast<bool>(entry.contact_nonce) +
+ TlType::CONTACT_NONCE_MASK * static_cast<bool>(entry.contact_nonce_hash);
+ return td::e2e_api::make_object<TlType>(flags, to_td(entry.self_nonce.value_or(Int256{})),
+ to_td(entry.contact_nonce_hash.value_or(Int256{})),
+ to_td(entry.contact_nonce.value_or(Int256{})));
+}
+
+inline auto to_tl(const ContactState &entry) {
+ // TODO
+ return td::e2e_api::make_object<td::e2e_api::e2e_personalContactState>(0, false);
+}
+
+template <class T>
+auto to_tl(const Entry<T> &entry) {
+ return td::e2e_api::make_object<td::e2e_api::e2e_personalOnClient>(entry.timestamp, to_tl(entry.value));
+}
+
+template <class T>
+auto to_tl(const SignedEntry<T> &entry) {
+ return td::e2e_api::make_object<td::e2e_api::e2e_personalOnServer>(to_td(entry.signature), entry.timestamp,
+ to_tl(entry.value));
+}
+inline auto to_tl(const Contact &contact) {
+ std::vector<td::e2e_api::object_ptr<td::e2e_api::e2e_personalOnClient>> entries;
+ if (contact.o_user_id) {
+ entries.push_back(to_tl(*contact.o_user_id));
+ }
+ if (contact.o_name) {
+ entries.push_back(to_tl(*contact.o_name));
+ }
+ if (contact.o_phone_number) {
+ entries.push_back(to_tl(*contact.o_phone_number));
+ }
+ entries.push_back(to_tl(contact.emoji_nonces));
+ entries.push_back(to_tl(contact.contact_state));
+ return td::e2e_api::make_object<td::e2e_api::e2e_valueContactByPublicKey>(std::move(entries));
+}
+
+inline Update to_update(Entry<UserId> user_id) {
+ Update result;
+ result.o_user_id = std::move(user_id);
+ return result;
+}
+inline Update to_update(Entry<Name> name) {
+ Update result;
+ result.o_name = std::move(name);
+ return result;
+}
+inline Update to_update(Entry<PhoneNumber> phone_number) {
+ Update result;
+ result.o_phone_number = std::move(phone_number);
+ return result;
+}
+inline Update to_update(Entry<EmojiNonces> emoji) {
+ Update result;
+ result.o_emoji_nonces = std::move(emoji);
+ return result;
+}
+inline Update to_update(Entry<ContactState> contact_state) {
+ Update result;
+ result.o_contact_state = std::move(contact_state);
+ return result;
+}
+
+inline td::StringBuilder &operator<<(td::StringBuilder &sb, const Name &entry) {
+ return sb << "Name{" << entry.first_name << " " << entry.last_name << "}";
+}
+inline td::StringBuilder &operator<<(td::StringBuilder &sb, const PhoneNumber &entry) {
+ return sb << "PhoneNumber{" << entry.phone_number << "}";
+}
+inline bool operator==(const Name &lhs, const Name &rhs) {
+ return lhs.first_name == rhs.first_name && lhs.last_name == rhs.last_name;
+}
+inline td::StringBuilder &operator<<(td::StringBuilder &sb, const EmojiNonces &entry) {
+ sb << "EmojiNonces{";
+ bool f = false;
+ if (entry.self_nonce) {
+ sb << "SelfNonce";
+ f = true;
+ }
+ if (entry.contact_nonce_hash) {
+ if (f) {
+ sb << "|";
+ }
+ sb << "TheirNonceHash";
+ f = true;
+ }
+ if (entry.contact_nonce) {
+ if (f) {
+ sb << "|";
+ }
+ sb << "ContactNonce";
+ f = true;
+ }
+ return sb << "}";
+}
+inline bool operator==(const PhoneNumber &lhs, const PhoneNumber &rhs) {
+ return lhs.phone_number == rhs.phone_number;
+}
+
+inline td::StringBuilder &operator<<(td::StringBuilder &sb, const ContactState &entry) {
+ switch (entry.state) {
+ case ContactState::Unknown:
+ return sb << "Unknown";
+ case ContactState::Contact:
+ return sb << "Contact";
+ case ContactState::NotContact:
+ return sb << "NotContact";
+ default:
+ UNREACHABLE();
+ }
+}
+inline bool operator==(const ContactState &lhs, const ContactState &rhs) {
+ return lhs.state == rhs.state;
+}
+
+template <class S>
+td::StringBuilder &operator<<(td::StringBuilder &sb, const Entry<S> &entry) {
+ sb << entry.value << "\t";
+ switch (entry.source) {
+ case Entry<S>::Self:
+ sb << "[Self]";
+ break;
+
+ case Entry<S>::Server:
+ sb << "[Server]";
+ break;
+
+ case Entry<S>::Contact:
+ sb << "[Contact]";
+ break;
+ default:
+ UNREACHABLE();
+ }
+ sb << "\tts=" << entry.timestamp;
+ return sb;
+}
+template <class T>
+bool operator==(const Entry<T> &lhs, const Entry<T> &rhs) {
+ return true;
+ //TODO(now)
+ // return lhs.source == rhs.source && lhs.value == rhs.value && lhs.timestamp == rhs.timestamp;
+}
+
+template <class S>
+td::StringBuilder &operator<<(td::StringBuilder &sb, const SignedEntry<S> &entry) {
+ sb << "[Signed]";
+ sb << " ts=" << entry.timestamp;
+ sb << " " << entry.value;
+ return sb;
+}
+inline td::StringBuilder &operator<<(td::StringBuilder &sb, const Contact &contact) {
+ sb << "\nContact{";
+ auto p = [&](auto &value) {
+ sb << "\n\t" << value;
+ };
+ auto op = [&](auto &o_value) {
+ if (o_value) {
+ p(*o_value);
+ }
+ };
+ op(contact.o_user_id);
+ op(contact.o_name);
+ op(contact.o_phone_number);
+ p(contact.emoji_nonces);
+ p(contact.contact_state);
+ return sb << "\n}";
+}
+
+inline bool operator==(const Contact &lhs, const Contact &rhs) {
+ return lhs.generation == rhs.generation && lhs.o_name == rhs.o_name && lhs.o_phone_number == rhs.o_phone_number &&
+ lhs.o_user_id == rhs.o_user_id && lhs.public_key == rhs.public_key && lhs.contact_state == rhs.contact_state &&
+ lhs.emoji_nonces == rhs.emoji_nonces;
+}
+} // namespace tde2e_api
+namespace tde2e_core {
+inline td::StringBuilder &operator<<(td::StringBuilder &sb, const Update &update) {
+ sb << "\nUpdate{";
+ auto p = [&](auto &value) {
+ sb << "\n\t" << value;
+ };
+ auto op = [&](auto &o_value) {
+ if (o_value) {
+ p(*o_value);
+ }
+ };
+ op(update.o_user_id);
+ op(update.o_name);
+ op(update.o_phone_number);
+ op(update.o_emoji_nonces);
+ op(update.o_contact_state);
+ return sb << "\n}\n";
+}
+inline bool operator==(const Update &lhs, const Update &rhs) {
+ return lhs.o_contact_state == rhs.o_contact_state && lhs.o_user_id == rhs.o_user_id &&
+ lhs.o_phone_number == rhs.o_phone_number && lhs.o_emoji_nonces == rhs.o_emoji_nonces &&
+ lhs.o_user_id == rhs.o_user_id;
+}
+
+struct KeyContactByPublicKey {
+ td::UInt256 public_key{};
+
+ KeyContactByPublicKey() = default;
+
+ explicit KeyContactByPublicKey(td::UInt256 public_key) : public_key(public_key) {
+ }
+
+ bool operator<(const KeyContactByPublicKey &other) const {
+ return public_key < other.public_key;
+ }
+
+ friend td::StringBuilder &operator<<(td::StringBuilder &sb, const KeyContactByPublicKey &key) {
+ return sb << "PubKey{" << td::base64_encode(key.public_key.as_slice()).substr(0, 8) << "}";
+ }
+};
+using Key = KeyContactByPublicKey;
+using Value = api::Contact;
+
+// TODO:
+// - delete value
+struct EncryptedStorage {
+ struct BlockchainState {
+ std::string next_block;
+ std::vector<std::string> need_proofs;
+ };
+ struct KeyValueUpdates {
+ std::vector<std::pair<Key, std::optional<Value>>> updates;
+ };
+ using UpdateId = td::int64;
+
+ static td::Result<EncryptedStorage> create(td::Slice last_block, PrivateKey pk) {
+ auto public_key = pk.to_public_key();
+ auto secret_for_key = MessageEncryption::hmac_sha512(pk.to_secure_string(), "EncryptedStorage::secret_for_key");
+ auto secret_for_value = MessageEncryption::hmac_sha512(pk.to_secure_string(), "EncryptedStorage::secret_for_value");
+ ClientBlockchain blockchain;
+ if (last_block.empty()) {
+ TRY_RESULT_ASSIGN(blockchain, ClientBlockchain::create_empty());
+ } else {
+ TRY_RESULT_ASSIGN(blockchain, ClientBlockchain::create_from_block(last_block, std::move(public_key)));
+ }
+ return EncryptedStorage(std::move(pk), std::move(secret_for_key), std::move(secret_for_value),
+ std::move(blockchain));
+ }
+
+ EncryptedStorage(PrivateKey pk, td::SecureString secret_for_key, td::SecureString secret_for_value,
+ ClientBlockchain blockchain)
+ : private_key_(std::move(pk))
+ , secret_for_key_(std::move(secret_for_key))
+ , secret_for_value_(std::move(secret_for_value))
+ , blockchain_(std::move(blockchain)) {
+ }
+
+ template <class T>
+ td::Result<UpdateId> update(Key key, api::SignedEntry<T> signed_entry) {
+ // verify signature
+ TRY_STATUS(verify_signature(PublicKey::from_u256(key.public_key), *to_tl(signed_entry)));
+
+ return update(
+ key, to_update(api::Entry<T>{api::Entry<T>::Contact, signed_entry.timestamp, std::move(signed_entry.value)}));
+ }
+
+ template <class T>
+ static td::Result<api::SignedEntry<T>> sign_entry(const PrivateKey &pk, api::Entry<T> entry) {
+ api::SignedEntry<T> signed_entry;
+ signed_entry.value = std::move(entry.value);
+ TRY_RESULT(signature, sign(pk, *to_tl(signed_entry)));
+ td::MutableSlice(signed_entry.signature.data(), signed_entry.signature.size()).copy_from(signature.to_slice());
+ return signed_entry;
+ }
+
+ td::Result<std::optional<Value>> get(Key key, bool optimistic = false);
+
+ // current blockchain height
+ td::int64 get_height() const;
+ // one should only apply blocks from server (TODO: signature from server?)
+ td::Result<KeyValueUpdates> apply_block(td::Slice block);
+
+ // proof must be from block of current height
+ // after proof is applied
+ // Keys are used as a hint
+ td::Status add_proof(td::Slice proof, td::Span<std::string> keys);
+
+ BlockchainState get_blockchain_state();
+ KeyValueUpdates pull_updates();
+
+ private:
+ struct UpdateInfo {
+ std::vector<UpdateId> update_ids;
+ Update update;
+ std::optional<Value> o_new_value;
+ };
+
+ std::map<Key, UpdateInfo> updates_;
+ std::map<Key, std::optional<Value>> partial_key_value_;
+ UpdateId next_update_id_{0};
+
+ PrivateKey private_key_;
+ td::SecureString secret_for_key_;
+ td::SecureString secret_for_value_;
+
+ ClientBlockchain blockchain_;
+
+ KeyValueUpdates pending_key_value_updates_;
+
+ td::Result<UpdateId> update(Key key, Update update);
+
+ td::Result<std::pair<Key, std::optional<Value>>> parse(td::Slice raw_key, td::Slice raw_value);
+ void sync_entry(Key key, std::optional<Value> value, bool rewrite = false);
+ bool reapply_update(UpdateInfo &update_info, const std::optional<Value> &value);
+
+ std::string encrypt_key(const Key &key) const;
+ std::string encrypt_value(const Value &value) const;
+ td::Result<Key> decrypt_key(td::Slice raw_key) const;
+ td::Result<std::optional<Value>> decrypt_value(td::Slice raw_value) const;
+};
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/Encryption.md b/tde2e/td/e2e/Encryption.md
new file mode 100644
index 000000000..4155a933e
--- /dev/null
+++ b/tde2e/td/e2e/Encryption.md
@@ -0,0 +1,183 @@
+# Encryption in Secret Group Calls
+
+A group call consists of three primary components:
+
+1. **Blockchain** shared among all group members. This serves as a synchronization point for all group changes and generates verification codes for MitM protection. Its hash incorporates the entire history of call changes, including the shared key hash. Each block contains call participant changes and new shared keys individually encrypted for each participant.
+
+2. **Encryption protocol** for network packets. Designed for efficiency, this protocol encrypts at the video and audio frame level rather than the network level. Each packet is signed to enable authorship verification. Similar encryption primitives secure the shared key for each participant.
+
+3. **Emoji generation protocol**. Direct generation of emojis from the blockchain hash is vulnerable to brute-force attacks by block creators. To mitigate this, we implement a two-phase protocol: first, each party commits to a value by publishing its hash; second, each party reveals their value. The combined hash of all values introduces unpredictable randomness into the blockchain hash.
+
+Let's examine each component in detail:
+
+## Blockchain
+
+The blockchain functions as a distributed ledger for managing group call state. Each block contains a participant list and new shared keys individually encrypted for all participants.
+
+The hash of the most recent block generates **verification words**.
+
+The hash of the most recent block, combined with unpredictable random values, generates **verification emojis**.
+
+For improved user experience, any person can currently join a call with server permission, without requiring explicit confirmation from existing participants.
+While the blockchain supports an explicit confirmation mode, we currently use `external_permissions` in the blockchain state to allow self-addition to groups.
+
+Call security in this scheme depends on emoji verification by each participant. This approach could be enhanced in future versions with persistent user identities.
+
+### Typical Workflow
+
+#### Joining a Call
+
+To join a call, a user must:
+- Request the latest blockchain block from the server
+- Create a new block containing updated state that includes themselves and a new shared key encrypted for all participants (including themselves)
+- Submit this block to the server
+
+The server validates that the block only adds the new user and attempts to apply it to the blockchain:
+
+- Upon success, all participants receive the new block, update their blockchain, and implement the new shared key
+- If conflicts exist (such as another block already applied at the same height), the operation fails
+
+#### Removing a Participant from the Call
+
+When a participant becomes inactive, they must be removed from the call. This process follows a similar pattern to joining but is initiated by any active participant.
+
+For comprehensive details, refer to the [Blockchain documentation](Blockchain.md).
+
+#### Security
+- Clients must only apply blocks as received from the server, which prevents blockchain forks when the server operates correctly
+- The blockchain state must be explicitly displayed in the UI, even when the server withholds information about certain participants
+- MitM protection relies on either verification words (currently not used in UI) or emojis; all participants must verify they see identical emojis
+- If the server delivers different blocks to different participants, the resulting fork hashes will permanently differ
+- The creator of a new key must be included as a participant in the block since they generate the shared key
+- Notably, participants cannot remove themselves from a group, as this would require generating a new shared key for the remaining participants
+- Active participants should remove inactive users from the group, particularly those blocking the emoji generation process
+- In the current implementation without explicit confirmations, signatures provide limited security value since anyone with a key can join a call; however, they enhance protocol robustness
+
+
+## Encryption
+
+### Core Primitives
+
+Our encryption system utilizes several primitives similar to MTProto 2.0. The key functions include:
+
+#### encrypt_data(payload, secret, extra_data) - encrypts payload with shared secret
+
+1) padding_size = ((16 + payload.size + 15) & -16) - payload.size
+2) padding = random_bytes(padding_size) with padding[0] = padding_size
+3) padded_data = padding || payload
+4) large_secret = KDF(secret, "tde2e_encrypt_data")
+5) encrypt_secret = large_secret[0:32]
+6) hmac_secret = large_secret[32:64]
+7) large_msg_id = HMAC-SHA256(hmac_secret, padded_data || extra_data || len(extra_data))
+8) msg_id = large_msg_id[0:16]
+9) (aes_key, aes_iv) = HMAC-SHA512(encrypt_secret, msg_id)[0:48]
+10) encrypted = aes_cbc(aes_key, aes_iv, padded_data)
+11) return (msg_id || encrypted), large_msg_id
+
+#### encrypt_header(header, encrypted_msg, secret) - encrypts 32-byte header
+
+1) msg_id = encrypted_msg[0:16] // First 16 bytes
+2) encrypt_secret = KDF(secret, "tde2e_encrypt_header")[0:32]
+3) (aes_key, aes_iv) = HMAC-SHA512(encrypt_secret, msg_id)[0:48]
+4) encrypted_header = aes_cbc(aes_key, aes_iv, header)
+
+Note: KDF refers to HMAC-SHA512 throughout this document
+
+#### Security
+- Verification of `msg_id` during decryption is essential before accepting any payload
+- Replay protection is implemented at a higher protocol level
+
+### Packet Encryption
+
+The encryption process for video and audio packets follows this sequence:
+
+#### encrypt_packet(payload, active_epochs, user_id, channel_id, seqno, private_key) - encrypts a packet
+
+First, generate header_a describing the epochs (hash of corresponding blockchain blocks) in use:
+1) epoch_id[i] = active_epochs[i].block_hash (32 bytes)
+2) header_a = active_epochs.size (4 bytes) || epoch_id[0] || epoch_id[1] || ...
+
+Next, encrypt the payload using a one-time key. And sign large_msg_id:
+1) one_time_key = random(32)
+2) packet_payload = channel_id (4 bytes) || seqno (4 bytes) || payload
+3) encrypted_payload, large_msg_id = encrypt_data(packet_payload, one_time_key, magic1 || header_a)
+4) to_sign = magic2 || large_msg_id
+5) signature = sign(to_sign, private_key) // 64 bytes
+
+magic1 is magic for `e2e.callPacket = e2e.CallPacket;`
+magic2 is magic for `e2e.callPacketLargeMsgId = e2e.CallPacketLargeMsgId;`
+
+Finally, encrypt the one-time key using the shared secret from each active epoch:
+1) encrypted_key[i] = encrypt_header(one_time_key, encrypted_payload, active_epochs[i].shared_key) (32 bytes)
+2) header_b = encrypted_key[0] || encrypted_key[1] || ...
+
+The complete packet format is: header_a || header_b || encrypted_payload || signature
+
+#### Security
+- The seqno value is unique for each (public key, channel_id) pair, providing protection against replay attacks; receivers should maintain records of recent seqno values and reject packets with known or outdated seqno values
+- During decryption, the public key must be retrieved from the blockchain state using the externally provided user_id; this public key verifies the signature
+
+### Shared Key Encryption
+
+When modifying group state or shared key, the new shared key is encrypted for each participant using their respective public keys from the blockchain state:
+
+1. Generate new cryptographic material:
+ - `raw_group_shared_key = random(32 bytes)` - the new shared key for the call
+ - `one_time_secret = random(32 bytes)` - secret used for encryption
+ - `e_private_key, e_public_key = generate_private_key()` - key pair used to encrypt the one_time_secret
+
+2. Encrypt the group shared key:
+ - `encrypted_raw_group_shared_key = encrypt_data(raw_group_shared_key, one_time_secret)`
+
+3. For each participant in the group:
+ - `shared_key = compute_shared_secret(e_private_key, participant.public_key)`
+ - `encrypted_header = encrypt_header(one_time_secret, encrypted_raw_group_shared_key, shared_key)`
+
+The `e_public_key`, `encrypted_raw_group_shared_key`, and `encrypted_header` for each participant are recorded in the blockchain state.
+
+`group_shared_key = HMAC-SHA512(raw_group_shared_key, block_hash)`
+
+#### Security
+- We cannot guarantee that every participant will successfully decrypt the key
+- However, all participants who can decrypt will obtain the identical `shared_secret`
+- Participants unable to decrypt the key must exit the call immediately, and specifically must not participate in the emoji generation process
+
+
+## Emoji Generation
+
+The emoji hash generation employs a two-phase commit-reveal protocol to prevent block creators from performing brute-force attacks.
+
+#### Protocol Workflow
+
+1. Initial Setup:
+ - Each participant generates a random 32-byte nonce
+ - `nonce_hash = SHA256(nonce)`
+
+2. Commit Phase:
+ - Each participant broadcasts their `nonce_hash` with a signature
+ - The system waits for all participants to submit commits
+ - Transition to the Reveal phase occurs only after receiving all commits
+
+3. Reveal Phase:
+ - Each participant broadcasts their original `nonce` with signature
+ - The system verifies that `SHA256(revealed_nonce) == committed_hash`
+ - The process waits for all participants to complete the reveal step
+
+4. Final Hash Generation:
+ - All revealed nonces are concatenated in order
+ - `emoji_hash = HMAC-SHA512(concatenated_sorted_nonces, blockchain_hash)`
+
+
+The TL schema for this broadcast mechanism is:
+```
+e2e.chain.groupBroadcastNonceCommit signature:int512 public_key:int256 chain_height:int32 chain_hash:int256 nonce_hash:int256 = e2e.chain.GroupBroadcast;
+e2e.chain.groupBroadcastNonceReveal signature:int512 public_key:int256 chain_height:int32 chain_hash:int256 nonce:int256 = e2e.chain.GroupBroadcast;
+```
+
+The signature applies to the TL serialization of the same object with a zeroed signature field.
+
+#### Security
+- The resulting `emoji_hash` remains completely unpredictable for all protocol participants
+- For simplicity and protection against bugs, participants should only apply messages (including those they created themselves) when received from the server; this approach ensures that when any participant sees a packet, all participants see the packet
+- Consequently, emojis won't be displayed before all clients with reasonable internet connections can also view them
+- The two-phase commit-reveal protocol prevents any participant from biasing the emoji selection toward a specific pattern
diff --git a/tde2e/td/e2e/Keys.cpp b/tde2e/td/e2e/Keys.cpp
new file mode 100644
index 000000000..e7aafd289
--- /dev/null
+++ b/tde2e/td/e2e/Keys.cpp
@@ -0,0 +1,208 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/e2e/Keys.h"
+
+#include "td/e2e/MessageEncryption.h"
+
+#include "td/utils/Ed25519.h"
+#include "td/utils/misc.h"
+#include "td/utils/SliceBuilder.h"
+
+namespace tde2e_core {
+
+struct PublicKeyRaw {
+ td::Ed25519::PublicKey public_key;
+};
+
+struct PrivateKeyRaw {
+ PublicKeyRaw public_key;
+ td::Ed25519::PrivateKey private_key;
+ std::shared_ptr<const td::Ed25519::PreparedPrivateKey> prepared_private_key;
+};
+
+struct PrivateKeyWithMnemonicRaw {
+ std::vector<td::SecureString> mnemonic;
+ PrivateKeyRaw key_pair;
+};
+
+Signature Signature::from_u512(const td::UInt512 &signature) {
+ return Signature{signature};
+}
+
+td::UInt512 Signature::to_u512() const {
+ return signature_;
+}
+
+td::Result<Signature> Signature::from_slice(const td::Slice &slice) {
+ td::UInt512 signature;
+ if (slice.size() != 64) {
+ return td::Status::Error(PSLICE() << "Invalid signature length: " << slice.size());
+ }
+ signature.as_mutable_slice().copy_from(slice);
+ return Signature{signature};
+}
+
+td::Slice Signature::to_slice() const {
+ return signature_.as_slice();
+}
+
+auto empty_public_key() {
+ static auto pk = PublicKey::from_u256({});
+ return pk;
+}
+
+PublicKey::PublicKey() : raw_(empty_public_key().raw_) {
+}
+
+PublicKey::PublicKey(std::shared_ptr<const PublicKeyRaw> public_key) : raw_(std::move(public_key)) {
+ CHECK(raw_);
+}
+
+td::Result<PublicKey> PublicKey::from_slice(td::Slice slice) {
+ if (slice.size() != td::Ed25519::PublicKey::LENGTH) {
+ return td::Status::Error("Invalid length of public key");
+ }
+ PublicKeyRaw public_key_raw{td::Ed25519::PublicKey(td::SecureString(slice))};
+ return PublicKey(std::make_shared<PublicKeyRaw>(std::move(public_key_raw)));
+}
+
+PublicKey PublicKey::from_u256(const td::UInt256 &public_key) {
+ PublicKeyRaw public_key_raw{td::Ed25519::PublicKey(td::SecureString(public_key.as_slice()))};
+ return PublicKey(std::make_shared<PublicKeyRaw>(std::move(public_key_raw)));
+}
+
+td::UInt256 PublicKey::to_u256() const {
+ CHECK(raw_);
+ td::UInt256 result;
+ result.as_mutable_slice().copy_from(raw_->public_key.as_octet_string());
+ return result;
+}
+
+td::Status PublicKey::verify(td::Slice data, const Signature &signature) const {
+ CHECK(raw_);
+ return raw_->public_key.verify_signature(data, signature.to_slice());
+}
+
+td::SecureString PublicKey::to_secure_string() const {
+ return raw_->public_key.as_octet_string();
+}
+
+bool PublicKey::operator==(const PublicKey &other) const {
+ return to_u256() == other.to_u256();
+}
+
+bool PublicKey::operator!=(const PublicKey &other) const {
+ return !(*this == other);
+}
+
+bool PublicKey::operator<(const PublicKey &other) const {
+ return to_u256() < other.to_u256();
+}
+
+auto empty_private_key() {
+ static auto pk = PrivateKey::from_slice(std::string(32, 1)).move_as_ok();
+ return pk;
+}
+
+PrivateKey::PrivateKey() : raw_(empty_private_key().raw_) {
+}
+
+PrivateKey::PrivateKey(std::shared_ptr<const PrivateKeyRaw> key_pair) : raw_(std::move(key_pair)) {
+ CHECK(raw_);
+}
+
+td::Result<PrivateKey> PrivateKey::generate() {
+ TRY_RESULT(private_key, td::Ed25519::generate_private_key());
+ TRY_RESULT(public_key, private_key.get_public_key());
+ TRY_RESULT(prepared_private_key, private_key.prepare());
+ return std::make_shared<PrivateKeyRaw>(
+ PrivateKeyRaw{{std::move(public_key)}, std::move(private_key), std::move(prepared_private_key)});
+}
+
+td::Result<PrivateKey> PrivateKey::from_slice(const td::Slice &slice) {
+ if (slice.size() != td::Ed25519::PublicKey::LENGTH) {
+ return td::Status::Error("Invalid private key length");
+ }
+ auto private_key = td::Ed25519::PrivateKey(td::SecureString(slice));
+ TRY_RESULT(public_key, private_key.get_public_key());
+ TRY_RESULT(prepared_private_key, private_key.prepare());
+ return std::make_shared<PrivateKeyRaw>(
+ PrivateKeyRaw{{std::move(public_key)}, std::move(private_key), std::move(prepared_private_key)});
+}
+
+td::Result<td::SecureString> PrivateKey::compute_shared_secret(const PublicKey &public_key) const {
+ TRY_RESULT(x25519_shared_secret, td::Ed25519::compute_shared_secret(public_key.raw().public_key, raw_->private_key));
+ return td::SecureString(
+ MessageEncryption::hmac_sha512("tde2e_shared_secret", x25519_shared_secret).as_slice().substr(0, 32));
+}
+
+td::Result<Signature> PrivateKey::sign(const td::Slice &data) const {
+ CHECK(raw_);
+ TRY_RESULT(signature, td::Ed25519::PrivateKey::sign(*raw_->prepared_private_key, data));
+ //TRY_RESULT(signature, raw_->private_key.sign(data));
+ return Signature::from_slice(signature);
+}
+
+PublicKey PrivateKey::to_public_key() const {
+ CHECK(raw_);
+ return PublicKey(std::shared_ptr<const PublicKeyRaw>(raw_, &raw_->public_key));
+}
+
+td::SecureString PrivateKey::to_secure_string() const {
+ return raw_->private_key.as_octet_string();
+}
+
+PrivateKeyWithMnemonic::PrivateKeyWithMnemonic(std::shared_ptr<const PrivateKeyWithMnemonicRaw> raw)
+ : raw_(std::move(raw)) {
+ CHECK(raw_);
+}
+
+PrivateKeyWithMnemonic PrivateKeyWithMnemonic::from_private_key(const PrivateKey &private_key,
+ std::vector<td::SecureString> words) {
+ return PrivateKeyWithMnemonic(std::make_shared<PrivateKeyWithMnemonicRaw>(PrivateKeyWithMnemonicRaw{
+ std::move(words),
+ PrivateKeyRaw{{td::Ed25519::PublicKey(td::SecureString(private_key.to_public_key().to_u256().as_slice()))},
+ td::Ed25519::PrivateKey(private_key.to_secure_string()),
+ private_key.raw().prepared_private_key}}));
+}
+
+PrivateKey PrivateKeyWithMnemonic::to_private_key() const {
+ return PrivateKey(std::shared_ptr<const PrivateKeyRaw>(raw_, &raw_->key_pair));
+}
+
+td::Span<td::SecureString> PrivateKeyWithMnemonic::words() const {
+ CHECK(raw_);
+ return raw_->mnemonic;
+}
+
+PublicKey PrivateKeyWithMnemonic::to_public_key() const {
+ return to_private_key().to_public_key();
+}
+
+td::Result<Signature> PrivateKeyWithMnemonic::sign(const td::Slice &data) const {
+ return to_private_key().sign(data);
+}
+
+td::StringBuilder &operator<<(td::StringBuilder &sb, const PrivateKeyWithMnemonic &key_pair_with_mnemonic) {
+ return sb << "EdPrivateKey(pub="
+ << td::hex_encode(key_pair_with_mnemonic.to_public_key().to_u256().as_slice().substr(0, 8)) << "...)";
+}
+
+td::StringBuilder &operator<<(td::StringBuilder &sb, const PrivateKey &key_pair) {
+ return sb << "EdPrivateKey(pub=" << td::hex_encode(key_pair.to_public_key().to_u256().as_slice().substr(0, 8))
+ << "...)";
+}
+
+td::StringBuilder &operator<<(td::StringBuilder &sb, const PublicKey &public_key) {
+ return sb << "EdPublicKey(" << td::hex_encode(public_key.to_u256().as_slice().substr(0, 8)) << "...)";
+}
+
+td::StringBuilder &operator<<(td::StringBuilder &sb, const Signature &signature) {
+ return sb << "Signature(" << td::hex_encode(signature.signature_.as_slice().substr(0, 8)) << "...)";
+}
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/Keys.h b/tde2e/td/e2e/Keys.h
new file mode 100644
index 000000000..ca864e74c
--- /dev/null
+++ b/tde2e/td/e2e/Keys.h
@@ -0,0 +1,104 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include "td/utils/common.h"
+#include "td/utils/SharedSlice.h"
+#include "td/utils/Slice.h"
+#include "td/utils/Span.h"
+#include "td/utils/Status.h"
+#include "td/utils/StringBuilder.h"
+#include "td/utils/UInt.h"
+
+#include <memory>
+
+namespace tde2e_core {
+
+// new shiny public/private key classes
+struct PublicKeyRaw;
+struct PrivateKeyRaw;
+struct PrivateKeyWithMnemonicRaw;
+
+class Signature {
+ public:
+ Signature() = default;
+ explicit Signature(td::UInt512 signature) : signature_(signature) {
+ }
+ static Signature from_u512(const td::UInt512 &signature);
+ td::UInt512 to_u512() const;
+ static td::Result<Signature> from_slice(const td::Slice &slice);
+ td::Slice to_slice() const;
+ friend td::StringBuilder &operator<<(td::StringBuilder &sb, const Signature &signature);
+
+ private:
+ td::UInt512 signature_{};
+};
+
+class PublicKey {
+ public:
+ PublicKey();
+ explicit PublicKey(std::shared_ptr<const PublicKeyRaw> public_key);
+ static td::Result<PublicKey> from_slice(td::Slice);
+ static PublicKey from_u256(const td::UInt256 &public_key);
+ td::UInt256 to_u256() const;
+ td::Status verify(td::Slice data, const Signature &signature) const;
+ td::SecureString to_secure_string() const;
+ bool operator==(const PublicKey &other) const;
+ bool operator!=(const PublicKey &other) const;
+ bool operator<(const PublicKey &other) const;
+ friend td::StringBuilder &operator<<(td::StringBuilder &sb, const PublicKey &public_key);
+
+ const PublicKeyRaw &raw() const {
+ CHECK(raw_);
+ return *raw_;
+ }
+
+ private:
+ std::shared_ptr<const PublicKeyRaw> raw_;
+};
+
+class PrivateKey {
+ public:
+ PrivateKey();
+ explicit PrivateKey(std::shared_ptr<const PrivateKeyRaw> key_pair);
+ explicit operator bool() const noexcept {
+ return static_cast<bool>(raw_);
+ }
+ static td::Result<PrivateKey> generate();
+
+ static td::Result<PrivateKey> from_slice(const td::Slice &slice);
+ td::Result<td::SecureString> compute_shared_secret(const PublicKey &public_key) const;
+ td::Result<Signature> sign(const td::Slice &data) const;
+ PublicKey to_public_key() const;
+ td::SecureString to_secure_string() const;
+ friend td::StringBuilder &operator<<(td::StringBuilder &sb, const PrivateKey &key_pair);
+ const PrivateKeyRaw &raw() const {
+ CHECK(raw_);
+ return *raw_;
+ }
+
+ private:
+ std::shared_ptr<const PrivateKeyRaw> raw_;
+};
+
+class PrivateKeyWithMnemonic {
+ public:
+ explicit PrivateKeyWithMnemonic(std::shared_ptr<const PrivateKeyWithMnemonicRaw> raw);
+ static PrivateKeyWithMnemonic from_private_key(const PrivateKey &private_key,
+ std::vector<td::SecureString> words = {});
+ PrivateKey to_private_key() const;
+ td::Span<td::SecureString> words() const;
+
+ PublicKey to_public_key() const;
+ td::Result<Signature> sign(const td::Slice &data) const;
+ friend td::StringBuilder &operator<<(td::StringBuilder &sb, const PrivateKeyWithMnemonic &key_pair_with_mnemonic);
+
+ private:
+ std::shared_ptr<const PrivateKeyWithMnemonicRaw> raw_;
+};
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/MessageEncryption.cpp b/tde2e/td/e2e/MessageEncryption.cpp
new file mode 100644
index 000000000..3da322abc
--- /dev/null
+++ b/tde2e/td/e2e/MessageEncryption.cpp
@@ -0,0 +1,196 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/e2e/MessageEncryption.h"
+
+#include "td/utils/as.h"
+#include "td/utils/common.h"
+#include "td/utils/misc.h"
+#include "td/utils/Random.h"
+#include "td/utils/SharedSlice.h"
+
+#include <utility>
+
+namespace tde2e_core {
+
+namespace {
+constexpr size_t MIN_PADDING = 16;
+} // namespace
+
+td::AesCbcState MessageEncryption::calc_aes_cbc_state_from_hash(td::Slice hash) {
+ CHECK(hash.size() >= 48);
+ td::SecureString key(32);
+ key.as_mutable_slice().copy_from(hash.substr(0, 32));
+ td::SecureString iv(16);
+ iv.as_mutable_slice().copy_from(hash.substr(32, 16));
+ return td::AesCbcState{key, iv};
+}
+
+td::SecureString MessageEncryption::gen_random_prefix(td::int64 data_size, td::int64 min_padding) {
+ td::SecureString buff(
+ td::narrow_cast<size_t>(((min_padding + 15 + data_size) & ~static_cast<td::int64>(15)) - data_size), '\0');
+ td::Random::secure_bytes(buff.as_mutable_slice());
+ buff.as_mutable_slice().ubegin()[0] = td::narrow_cast<td::uint8>(buff.size());
+ CHECK((buff.size() + data_size) % 16 == 0);
+ return buff;
+}
+
+td::SecureString MessageEncryption::gen_deterministic_prefix(td::int64 data_size, td::int64 min_padding) {
+ td::SecureString buff(
+ td::narrow_cast<size_t>(((min_padding + 15 + data_size) & ~static_cast<td::int64>(15)) - data_size), '\0');
+ buff.as_mutable_slice().ubegin()[0] = td::narrow_cast<td::uint8>(buff.size());
+ CHECK((buff.size() + data_size) % 16 == 0);
+ return buff;
+}
+
+td::SecureString MessageEncryption::kdf(td::Slice secret, td::Slice password, int iterations) {
+ td::SecureString new_secret(64);
+ pbkdf2_sha512(secret, password, iterations, new_secret.as_mutable_slice());
+ return new_secret;
+}
+
+td::SecureString MessageEncryption::encrypt_data_with_prefix(td::Slice data, td::Slice secret, td::Slice extra,
+ td::UInt256 *save_large_msg_id) {
+ CHECK(data.size() % 16 == 0);
+ auto large_secret = kdf_expand(secret, "tde2e_encrypt_data");
+ auto encrypt_secret = large_secret.as_slice().substr(0, 32);
+ auto hmac_secret = large_secret.as_mutable_slice().substr(32, 32);
+
+ td::SecureString tail_data(data.size() + extra.size() + 4, '\0');
+ auto tail = tail_data.as_mutable_slice();
+ tail.copy_from(data);
+ tail.remove_prefix(data.size());
+ tail.copy_from(extra);
+ tail.remove_prefix(extra.size());
+ CHECK(tail.size() == 4);
+ td::as<td::int32>(tail.data()) = td::narrow_cast<td::int32>(extra.size());
+ auto large_msg_id = hmac_sha256(hmac_secret, tail_data);
+ if (save_large_msg_id) {
+ save_large_msg_id->as_mutable_slice().copy_from(large_msg_id);
+ }
+
+ auto msg_id = large_msg_id.as_slice().substr(0, 16);
+
+ td::SecureString res_buf(data.size() + 16, '\0');
+ auto res = res_buf.as_mutable_slice();
+ res.copy_from(msg_id);
+
+ auto cbc_state = calc_aes_cbc_state_from_hash(hmac_sha512(encrypt_secret, msg_id));
+ cbc_state.encrypt(data, res.substr(16));
+
+ return res_buf;
+}
+td::SecureString MessageEncryption::kdf_expand(td::Slice random_secret, td::Slice info) {
+ return hmac_sha512(random_secret, info);
+}
+
+td::SecureString MessageEncryption::encrypt_data(td::Slice data, td::Slice secret, td::Slice additional_data,
+ td::UInt256 *save_large_msg_id) {
+ auto prefix = gen_random_prefix(data.size(), MIN_PADDING);
+ td::SecureString combined(prefix.size() + data.size());
+ combined.as_mutable_slice().copy_from(prefix);
+ combined.as_mutable_slice().substr(prefix.size()).copy_from(data);
+ return encrypt_data_with_prefix(combined.as_slice(), secret, additional_data, save_large_msg_id);
+}
+
+td::Result<td::SecureString> MessageEncryption::decrypt_data(td::Slice encrypted_data, td::Slice secret,
+ td::Slice extra, td::UInt256 *save_large_msg_id) {
+ if (encrypted_data.size() < 16) {
+ return td::Status::Error("Failed to decrypt: encrypted_data is less than 16 bytes");
+ }
+ if (encrypted_data.size() % 16 != 0) {
+ return td::Status::Error("Failed to decrypt: data size is not divisible by 16");
+ }
+
+ auto large_secret = kdf_expand(secret, "tde2e_encrypt_data");
+ auto encrypt_secret = large_secret.as_slice().substr(0, 32);
+ auto hmac_secret = large_secret.as_mutable_slice().substr(32, 32);
+
+ auto msg_id = encrypted_data.substr(0, 16);
+ encrypted_data = encrypted_data.substr(16);
+
+ td::SecureString buf(encrypted_data.size() + extra.size() + 4, '\0');
+ auto decrypted_data = buf.as_mutable_slice().substr(0, encrypted_data.size());
+ buf.as_mutable_slice().substr(decrypted_data.size()).copy_from(extra);
+ td::as<td::int32>(buf.data() + decrypted_data.size() + extra.size()) = td::narrow_cast<td::int32>(extra.size());
+
+ auto cbc_state = calc_aes_cbc_state_from_hash(hmac_sha512(encrypt_secret, msg_id));
+ cbc_state.decrypt(encrypted_data, decrypted_data);
+
+ auto expected_large_msg_id = hmac_sha256(hmac_secret, buf);
+ auto expected_msg_id = expected_large_msg_id.as_slice().substr(0, 16);
+
+ // check hash
+ int is_mac_bad = 0;
+ for (size_t i = 0; i < 16; i++) {
+ is_mac_bad |= expected_msg_id[i] ^ msg_id[i];
+ }
+ if (is_mac_bad != 0) {
+ return td::Status::Error("Failed to decrypt: msg_id mismatch");
+ }
+ if (save_large_msg_id) {
+ save_large_msg_id->as_mutable_slice().copy_from(expected_large_msg_id);
+ }
+
+ auto prefix_size = static_cast<td::uint8>(decrypted_data[0]);
+ if (prefix_size > decrypted_data.size() || prefix_size < MIN_PADDING) {
+ return td::Status::Error("Failed to decrypt: invalid prefix size");
+ }
+
+ return td::SecureString(decrypted_data.substr(prefix_size));
+}
+
+td::SecureString MessageEncryption::hmac_sha512(td::Slice key, td::Slice message) {
+ td::SecureString res(64, 0);
+ td::hmac_sha512(key, message, res.as_mutable_slice());
+ return res;
+}
+td::SecureString MessageEncryption::hmac_sha256(td::Slice key, td::Slice message) {
+ td::SecureString res(32, 0);
+ td::hmac_sha256(key, message, res.as_mutable_slice());
+ return res;
+}
+
+td::Result<td::SecureString> MessageEncryption::encrypt_header(td::Slice decrypted_header, td::Slice encrypted_message,
+ td::Slice secret) {
+ if (encrypted_message.size() < 16) {
+ return td::Status::Error("Failed to encrypt header: encrypted_message is too small");
+ }
+ if (decrypted_header.size() != 32) {
+ return td::Status::Error("Failed to encrypt header: header must be 32 bytes");
+ }
+ auto large_key = kdf_expand(secret, "tde2e_encrypt_header");
+ auto encryption_key = large_key.as_slice().substr(0, 32);
+
+ auto msg_id = encrypted_message.substr(0, 16);
+ auto cbc_state = calc_aes_cbc_state_from_hash(kdf_expand(encryption_key, msg_id));
+
+ td::SecureString encrypted_header(32, 0);
+ cbc_state.encrypt(decrypted_header, encrypted_header.as_mutable_slice());
+ return encrypted_header;
+}
+
+td::Result<td::SecureString> MessageEncryption::decrypt_header(td::Slice encrypted_header, td::Slice encrypted_message,
+ td::Slice secret) {
+ if (encrypted_message.size() < 16) {
+ return td::Status::Error("Failed to decrypt: invalid message size");
+ }
+ if (encrypted_header.size() != 32) {
+ return td::Status::Error("Failed to decrypt: invalid header size");
+ }
+
+ auto large_key = kdf_expand(secret, "tde2e_encrypt_header");
+ auto encryption_key = large_key.as_slice().substr(0, 32);
+
+ auto msg_id = encrypted_message.substr(0, 16);
+ auto cbc_state = calc_aes_cbc_state_from_hash(hmac_sha512(encryption_key, msg_id));
+
+ td::SecureString decrypted_header(32, 0);
+ cbc_state.decrypt(encrypted_header, decrypted_header.as_mutable_slice());
+ return decrypted_header;
+}
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/MessageEncryption.h b/tde2e/td/e2e/MessageEncryption.h
new file mode 100644
index 000000000..34e7f4c81
--- /dev/null
+++ b/tde2e/td/e2e/MessageEncryption.h
@@ -0,0 +1,46 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include "td/utils/crypto.h"
+#include "td/utils/SharedSlice.h"
+#include "td/utils/Slice.h"
+#include "td/utils/Status.h"
+#include "td/utils/UInt.h"
+
+namespace tde2e_core {
+
+class MessageEncryption {
+ public:
+ static td::SecureString encrypt_data(td::Slice data, td::Slice secret, td::Slice additional_data = {},
+ td::UInt256 *save_large_msg_id = nullptr);
+ static td::Result<td::SecureString> decrypt_data(td::Slice encrypted_data, td::Slice secret,
+ td::Slice additional_data = {},
+ td::UInt256 *save_large_msg_id = nullptr);
+ static td::SecureString hmac_sha512(td::Slice key, td::Slice message);
+ static td::SecureString hmac_sha256(td::Slice key, td::Slice message);
+ static td::SecureString kdf(td::Slice secret, td::Slice password, int iterations);
+ static td::Result<td::SecureString> encrypt_header(td::Slice decrypted_header, td::Slice encrypted_message,
+ td::Slice secret);
+ static td::Result<td::SecureString> decrypt_header(td::Slice encrypted_header, td::Slice encrypted_message,
+ td::Slice secret);
+
+ private:
+ static td::AesCbcState calc_aes_cbc_state_from_hash(td::Slice hash);
+ static td::SecureString gen_random_prefix(td::int64 data_size, td::int64 min_padding);
+ static td::SecureString gen_deterministic_prefix(td::int64 data_size, td::int64 min_padding);
+
+ static td::SecureString encrypt_data_with_prefix(td::Slice data, td::Slice secret, td::Slice additional_data = {},
+ td::UInt256 *save_large_msg_id = nullptr);
+
+ static td::SecureString kdf_expand(td::Slice random_secret, td::Slice info);
+
+ friend class SimpleEncryptionV2;
+ friend class EncryptionTest;
+};
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/Mnemonic.cpp b/tde2e/td/e2e/Mnemonic.cpp
new file mode 100644
index 000000000..b1c0cc45b
--- /dev/null
+++ b/tde2e/td/e2e/Mnemonic.cpp
@@ -0,0 +1,264 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/e2e/Mnemonic.h"
+
+#include "td/e2e/bip39.h"
+#include "td/e2e/MessageEncryption.h"
+
+#include "td/utils/algorithm.h"
+#include "td/utils/common.h"
+#include "td/utils/crypto.h"
+#include "td/utils/format.h"
+#include "td/utils/logging.h"
+#include "td/utils/misc.h"
+#include "td/utils/optional.h"
+#include "td/utils/Random.h"
+#include "td/utils/ScopeGuard.h"
+#include "td/utils/SliceBuilder.h"
+#include "td/utils/Span.h"
+#include "td/utils/Timer.h"
+
+#include <algorithm>
+#include <vector>
+
+namespace tde2e_core {
+
+td::Result<Mnemonic> Mnemonic::create(td::SecureString words, td::SecureString password) {
+ return create_from_normalized(normalize_and_split(std::move(words)), std::move(password));
+}
+td::Result<Mnemonic> Mnemonic::create(std::vector<td::SecureString> words, td::SecureString password) {
+ return create(join(words), std::move(password));
+}
+Mnemonic::Options::Options() = default;
+td::Result<Mnemonic> Mnemonic::create_from_normalized(const std::vector<td::SecureString> &words,
+ td::SecureString password) {
+ auto new_words = normalize_and_split(join(words));
+ if (new_words != words) {
+ return td::Status::Error("Mnemonic string is not normalized");
+ }
+ return Mnemonic(std::move(new_words), std::move(password));
+}
+
+td::SecureString Mnemonic::to_entropy() const {
+ td::SecureString res(64);
+ td::hmac_sha512(join(words_), password_, res.as_mutable_slice());
+ return res;
+}
+
+td::SecureString Mnemonic::to_seed() const {
+ td::SecureString hash(64);
+ td::pbkdf2_sha512(as_slice(to_entropy()), "tde2e default seed", PBKDF_ITERATIONS, hash.as_mutable_slice());
+ return hash;
+}
+
+PrivateKey Mnemonic::to_private_key() const {
+ return PrivateKey::from_slice(to_seed().as_slice().substr(0, 32)).move_as_ok();
+}
+
+bool Mnemonic::is_basic_seed() const {
+ td::SecureString hash(64);
+ td::pbkdf2_sha512(as_slice(to_entropy()), "tde2e seed version", td::max(1, PBKDF_ITERATIONS / 256),
+ hash.as_mutable_slice());
+ return hash.as_slice()[0] == 0;
+}
+
+bool Mnemonic::is_password_seed() const {
+ td::SecureString hash(64);
+ td::pbkdf2_sha512(as_slice(to_entropy()), "tde2e fast seed version", 1, hash.as_mutable_slice());
+ return hash.as_slice()[0] == 1;
+}
+
+std::vector<td::SecureString> Mnemonic::get_words() const {
+ return td::transform(words_, [](const auto &word) { return word.copy(); });
+}
+td::SecureString Mnemonic::get_words_string() const {
+ CHECK(words_.size() > 0);
+ size_t length = words_.size() - 1;
+ for (auto &word : words_) {
+ length += word.size();
+ }
+ td::SecureString res(length);
+ auto dest = res.as_mutable_slice();
+ bool is_first = true;
+ for (auto &word : words_) {
+ if (!is_first) {
+ dest[0] = ' ';
+ dest.remove_prefix(1);
+ } else {
+ is_first = false;
+ }
+ dest.copy_from(word);
+ dest.remove_prefix(word.size());
+ }
+ return res;
+}
+
+std::vector<td::SecureString> Mnemonic::normalize_and_split(td::SecureString words) {
+ for (auto &c : words.as_mutable_slice()) {
+ if (td::is_alpha(c)) {
+ c = td::to_lower(c);
+ } else {
+ c = ' ';
+ }
+ }
+ auto vec = td::full_split(words.as_slice(), ' ');
+ std::vector<td::SecureString> res;
+ for (auto &s : vec) {
+ if (!s.empty()) {
+ res.emplace_back(s);
+ }
+ }
+ return res;
+}
+
+td::StringBuilder &operator<<(td::StringBuilder &sb, const Mnemonic &mnemonic) {
+ sb << "Mnemonic" << td::format::as_array(mnemonic.words_);
+ if (!mnemonic.password_.empty()) {
+ sb << " with password[" << mnemonic.password_ << "]";
+ }
+ return sb;
+}
+
+Mnemonic::Mnemonic(std::vector<td::SecureString> words, td::SecureString password)
+ : words_(std::move(words)), password_(std::move(password)) {
+}
+td::SecureString Mnemonic::join(td::Span<td::SecureString> words) {
+ size_t res_size = 0;
+ for (size_t i = 0; i < words.size(); i++) {
+ if (i != 0) {
+ res_size++;
+ }
+ res_size += words[i].size();
+ }
+ td::SecureString res(res_size);
+ auto dst = res.as_mutable_slice();
+ for (size_t i = 0; i < words.size(); i++) {
+ if (i != 0) {
+ dst[0] = ' ';
+ dst.remove_prefix(1);
+ }
+ dst.copy_from(words[i].as_slice());
+ dst.remove_prefix(words[i].size());
+ }
+ return res;
+}
+
+td::Span<std::string> Mnemonic::word_hints(td::Slice prefix) {
+ static std::vector<std::string> words = [] {
+ auto bip_words = Mnemonic::normalize_and_split(td::SecureString(bip39_english()));
+ return td::transform(bip_words, [](const auto &word) { return word.as_slice().str(); });
+ }();
+ if (prefix.empty()) {
+ return words;
+ }
+
+ auto p = std::equal_range(words.begin(), words.end(), prefix, [&](td::Slice a, td::Slice b) {
+ return a.truncate(prefix.size()) < b.truncate(prefix.size());
+ });
+
+ return td::Span<std::string>(&*p.first, p.second - p.first);
+}
+
+std::vector<std::string> Mnemonic::generate_verification_words(td::Slice data) {
+ static constexpr size_t VERIFICATION_WORD_COUNT = 24;
+ static constexpr size_t BITS_PER_WORD = 11;
+ static constexpr size_t BIP_WORD_COUNT = 1 << BITS_PER_WORD;
+ static constexpr size_t HASH_SIZE = 64;
+ static_assert(VERIFICATION_WORD_COUNT * BITS_PER_WORD <= HASH_SIZE * 8, "Verification words count is too large");
+
+ static auto bip_words = Mnemonic::normalize_and_split(td::SecureString(bip39_english()));
+ CHECK(bip_words.size() == BIP_WORD_COUNT);
+
+ auto hash = MessageEncryption::hmac_sha512("MnemonicVerificationWords", data);
+ CHECK(hash.size() == HASH_SIZE);
+
+ std::vector<std::string> verification_words;
+
+ std::size_t bit_pos = 0;
+ for (size_t i = 0; i < VERIFICATION_WORD_COUNT; ++i) {
+ td::uint16 index = 0;
+ for (size_t bit = 0; bit < BITS_PER_WORD; ++bit, ++bit_pos) {
+ if ((hash[bit_pos / 8] >> (bit_pos % 8)) & 1) {
+ index |= (1 << bit);
+ }
+ }
+ verification_words.push_back(bip_words.at(index % 2048).as_slice().str());
+ }
+ CHECK(bit_pos <= hash.size() * 8);
+
+ return verification_words;
+}
+
+td::Result<Mnemonic> Mnemonic::create_new(Options options) {
+ td::Timer timer;
+ if (options.words_count == 0) {
+ options.words_count = 24;
+ }
+ if (options.words_count < 8 || options.words_count > 48) {
+ return td::Status::Error(PSLICE() << "Invalid words count(" << options.words_count
+ << ") requested for mnemonic creation");
+ }
+ td::int32 max_iterations = 256 * 20;
+ if (!options.password.empty()) {
+ max_iterations *= 256;
+ }
+
+ td::Random::add_seed(options.entropy.as_slice());
+ SCOPE_EXIT {
+ td::Random::secure_cleanup();
+ };
+
+ auto bip_words = Mnemonic::normalize_and_split(td::SecureString(bip39_english()));
+ CHECK(bip_words.size() == 2048);
+
+ int A = 0, B = 0, C = 0;
+ for (int iteration = 0; iteration < max_iterations; iteration++) {
+ std::vector<td::SecureString> words;
+ td::SecureString rnd((options.words_count * 11 + 7) / 8);
+ td::Random::secure_bytes(rnd.as_mutable_slice());
+ for (int i = 0; i < options.words_count; i++) {
+ size_t word_i = 0;
+ for (size_t j = 0; j < 11; j++) {
+ size_t offset = i * 11 + j;
+ if ((rnd[offset / 8] & (1 << (offset & 7))) != 0) {
+ word_i |= 1 << j;
+ }
+ }
+ words.push_back(bip_words[word_i].copy());
+ }
+
+ bool has_password = !options.password.empty();
+
+ td::optional<Mnemonic> mnemonic_without_password;
+ if (has_password) {
+ auto copy_words = td::transform(words, [](auto &w) { return w.copy(); });
+ mnemonic_without_password = Mnemonic::create(std::move(copy_words), {}).move_as_ok();
+ if (!mnemonic_without_password.value().is_password_seed()) {
+ A++;
+ continue;
+ }
+ }
+
+ auto mnemonic = Mnemonic::create(std::move(words), options.password.copy()).move_as_ok();
+
+ if (!mnemonic.is_basic_seed()) {
+ B++;
+ continue;
+ }
+
+ if (has_password && mnemonic_without_password.value().is_basic_seed()) {
+ C++;
+ continue;
+ }
+
+ LOG(INFO) << "Mnemonic generation debug stats: " << A << " " << B << " " << C << " " << timer;
+ return std::move(mnemonic);
+ }
+ return td::Status::Error("Failed to create a mnemonic (must not happen)");
+}
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/Mnemonic.h b/tde2e/td/e2e/Mnemonic.h
new file mode 100644
index 000000000..79534ee0d
--- /dev/null
+++ b/tde2e/td/e2e/Mnemonic.h
@@ -0,0 +1,59 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include "td/e2e/Keys.h"
+
+#include "td/utils/SharedSlice.h"
+#include "td/utils/Slice.h"
+#include "td/utils/Span.h"
+#include "td/utils/Status.h"
+#include "td/utils/StringBuilder.h"
+
+namespace tde2e_core {
+
+class Mnemonic {
+ public:
+ static constexpr int PBKDF_ITERATIONS = 100000;
+ static td::Result<Mnemonic> create(td::SecureString words, td::SecureString password);
+ static td::Result<Mnemonic> create(std::vector<td::SecureString> words, td::SecureString password);
+ struct Options {
+ Options();
+ int words_count = 24;
+ td::SecureString password;
+ td::SecureString entropy;
+ };
+ static td::Result<Mnemonic> create_new(Options options = {});
+
+ td::SecureString to_entropy() const;
+
+ td::SecureString to_seed() const;
+
+ PrivateKey to_private_key() const;
+
+ bool is_basic_seed() const;
+ bool is_password_seed() const;
+
+ std::vector<td::SecureString> get_words() const;
+ td::SecureString get_words_string() const;
+
+ static std::vector<td::SecureString> normalize_and_split(td::SecureString words);
+ static td::Span<std::string> word_hints(td::Slice prefix);
+ static std::vector<std::string> generate_verification_words(td::Slice data);
+
+ private:
+ std::vector<td::SecureString> words_;
+ td::SecureString password_;
+
+ Mnemonic(std::vector<td::SecureString> words, td::SecureString password);
+ static td::SecureString join(td::Span<td::SecureString> words);
+ static td::Result<Mnemonic> create_from_normalized(const std::vector<td::SecureString> &words,
+ td::SecureString password);
+ friend td::StringBuilder &operator<<(td::StringBuilder &sb, const Mnemonic &mnemonic);
+};
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/QRHandshake.cpp b/tde2e/td/e2e/QRHandshake.cpp
new file mode 100644
index 000000000..c155c05b5
--- /dev/null
+++ b/tde2e/td/e2e/QRHandshake.cpp
@@ -0,0 +1,227 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/e2e/QRHandshake.h"
+
+#include "td/e2e/e2e_api.h"
+#include "td/e2e/MessageEncryption.h"
+
+#include "td/telegram/e2e_api.h"
+
+#include "td/utils/common.h"
+#include "td/utils/tl_parsers.h"
+
+namespace tde2e_core {
+
+namespace e2e = td::e2e_api;
+
+QRHandshakeBob::QRHandshakeBob(td::int64 bob_user_id, PrivateKey &&bob_private_key)
+ : bob_ephemeral_private_key_(PrivateKey::generate().move_as_ok())
+ , bob_private_key_(std::move(bob_private_key))
+ , bob_user_id_(bob_user_id)
+ , bob_nonce_(generate_nonce()) {
+}
+
+QRHandshakeBob QRHandshakeBob::create(td::int64 bob_user_id, PrivateKey bob_private_key) {
+ return QRHandshakeBob(bob_user_id, std::move(bob_private_key));
+}
+
+std::string QRHandshakeBob::generate_start() const {
+ return serialize_boxed(e2e::e2e_handshakeQR(bob_ephemeral_private_key_.to_public_key().to_u256(), bob_nonce_));
+}
+
+td::Result<td::SecureString> QRHandshakeBob::receive_accept(td::int64 alice_user_id, PublicKey alice_public_key,
+ td::Slice encrypted_accept) {
+ if (had_accept_) {
+ return td::Status::Error("Already processed accept");
+ }
+ had_accept_ = true;
+
+ CHECK(!o_alice_public_key_);
+ CHECK(!o_alice_user_id_);
+ CHECK(!o_alice_nonce_);
+
+ o_alice_public_key_ = std::move(alice_public_key);
+ o_alice_user_id_ = alice_user_id;
+
+ TRY_RESULT_ASSIGN(o_ephemeral_shared_secret_, bob_ephemeral_private_key_.compute_shared_secret(*o_alice_public_key_));
+ TRY_RESULT(shared_secret_tmp, bob_private_key_.compute_shared_secret(*o_alice_public_key_));
+ o_shared_secret_ = MessageEncryption::hmac_sha512(o_ephemeral_shared_secret_.value(), shared_secret_tmp);
+
+ TRY_RESULT(decrypted_accept, decrypt_ephemeral(encrypted_accept));
+ td::TlParser parser(decrypted_accept);
+ auto message = e2e::e2e_HandshakePrivate::fetch(parser);
+ TRY_STATUS_PREFIX(parser.get_status(), "Failed to parse message: ");
+ if (message->get_id() != e2e::e2e_handshakePrivateAccept::ID) {
+ return td::Status::Error("Unexpected public message type");
+ }
+ auto accept = td::move_tl_object_as<e2e::e2e_handshakePrivateAccept>(message);
+ CHECK(accept);
+
+ o_alice_nonce_ = accept->alice_nonce_;
+
+ CHECK(o_alice_user_id_);
+ CHECK(o_alice_public_key_);
+ CHECK(o_alice_nonce_);
+
+ if (accept->bob_nonce_ != bob_nonce_) {
+ return td::Status::Error("Bob's nonce mismatch");
+ }
+ if (PublicKey::from_u256(accept->alice_PK_) != *o_alice_public_key_) {
+ return td::Status::Error("Alice's public key mismatch");
+ }
+ if (PublicKey::from_u256(accept->bob_PK_) != bob_private_key_.to_public_key()) {
+ return td::Status::Error("Bob's public key mismatch");
+ }
+ if (accept->alice_user_id_ != *o_alice_user_id_) {
+ return td::Status::Error("Alice's user_id mismatch");
+ }
+ if (accept->bob_user_id_ != bob_user_id_) {
+ return td::Status::Error("Bob's user_id mismatch");
+ }
+
+ auto decrypted_message = serialize_boxed(
+ e2e::e2e_handshakePrivateFinish(o_alice_public_key_.value().to_u256(), bob_private_key_.to_public_key().to_u256(),
+ *o_alice_user_id_, bob_user_id_, *o_alice_nonce_, bob_nonce_));
+ return encrypt(decrypted_message);
+}
+
+td::SecureString QRHandshakeBob::encrypt(td::Slice data) const {
+ CHECK(o_shared_secret_);
+ return MessageEncryption::encrypt_data(data, *o_shared_secret_);
+}
+
+td::Result<td::SecureString> QRHandshakeBob::decrypt(td::Slice encrypted_message) const {
+ if (!o_shared_secret_) {
+ return td::Status::Error("Have no shared secret (handshake is in progress)");
+ }
+ return MessageEncryption::decrypt_data(encrypted_message, *o_shared_secret_);
+}
+
+td::Result<td::SecureString> QRHandshakeBob::decrypt_ephemeral(td::Slice encrypted_message) const {
+ if (!o_ephemeral_shared_secret_) {
+ return td::Status::Error("Have no ephemeral shared secret (handshake is in progress)");
+ }
+ return MessageEncryption::decrypt_data(encrypted_message, *o_ephemeral_shared_secret_);
+}
+
+QRHandshakeAlice::QRHandshakeAlice(td::int64 alice_user_id, PrivateKey &&alice_private_key, td::int64 bob_user_id,
+ PublicKey &&bob_public_key, const td::UInt256 &bob_nonce,
+ td::SecureString &&ephemeral_shared_secret, td::SecureString &&shared_secret)
+ : alice_private_key_(std::move(alice_private_key))
+ , alice_user_id_(alice_user_id)
+ , alice_nonce_(generate_nonce())
+ , bob_public_key_(std::move(bob_public_key))
+ , bob_user_id_(bob_user_id)
+ , bob_nonce_(bob_nonce)
+ , ephemeral_shared_secret_(std::move(ephemeral_shared_secret))
+ , shared_secret_(std::move(shared_secret)) {
+}
+
+td::Result<QRHandshakeAlice> QRHandshakeAlice::create(td::int64 alice_user_id, PrivateKey alice_private_key,
+ td::int64 bob_user_id, PublicKey bob_public_key,
+ td::Slice serialized_qr) {
+ auto alice_public_key = alice_private_key.to_public_key();
+ td::TlParser parser(serialized_qr);
+ auto message = e2e::e2e_HandshakePublic::fetch(parser);
+ TRY_STATUS_PREFIX(parser.get_status(), "Failed to parse public qr: ");
+ if (message->get_id() != e2e::e2e_handshakeQR::ID) {
+ return td::Status::Error("Unexpected public message type");
+ }
+ auto qr = td::move_tl_object_as<e2e::e2e_handshakeQR>(message);
+ CHECK(qr);
+
+ auto bob_ephemeral_public_key = PublicKey::from_u256(qr->bob_ephemeral_PK_);
+ TRY_RESULT(ephemeral_shared_secret, alice_private_key.compute_shared_secret(bob_ephemeral_public_key));
+ TRY_RESULT(shared_secret_tmp, alice_private_key.compute_shared_secret(bob_public_key));
+ auto shared_secret = MessageEncryption::hmac_sha512(ephemeral_shared_secret, shared_secret_tmp);
+ return QRHandshakeAlice{alice_user_id,
+ std::move(alice_private_key),
+ bob_user_id,
+ std::move(bob_public_key),
+ qr->bob_nonce_,
+ std::move(ephemeral_shared_secret),
+ std::move(shared_secret)};
+}
+
+td::string QRHandshakeAlice::serialize_login_import(td::Slice accept, td::Slice encrypted_alice_pk) {
+ return serialize_boxed(e2e::e2e_handshakeLoginExport(accept.str(), encrypted_alice_pk.str()));
+}
+
+td::Result<std::pair<td::string, td::string>> QRHandshakeAlice::deserialize_login_import(td::Slice data) {
+ td::TlParser parser(data);
+ auto message = e2e::e2e_HandshakePublic::fetch(parser);
+ TRY_STATUS_PREFIX(parser.get_status(), "Failed to parse message: ");
+ if (message->get_id() != e2e::e2e_handshakeLoginExport::ID) {
+ return td::Status::Error("Unexpected public message type");
+ }
+ auto login_export = td::move_tl_object_as<e2e::e2e_handshakeLoginExport>(message);
+ CHECK(login_export);
+ return std::make_pair(login_export->accept_, login_export->encrypted_key_);
+}
+
+td::SecureString QRHandshakeAlice::generate_accept() const {
+ auto decrypted_message = serialize_boxed(e2e::e2e_handshakePrivateAccept(alice_private_key_.to_public_key().to_u256(),
+ bob_public_key_.to_u256(), alice_user_id_,
+ bob_user_id_, alice_nonce_, bob_nonce_));
+ return encrypt_ephemeral(decrypted_message);
+}
+
+td::Status QRHandshakeAlice::receive_finish(td::Slice encrypted_finish) {
+ if (had_finish_) {
+ return td::Status::Error("Already processed finish");
+ }
+ had_finish_ = true;
+
+ TRY_RESULT(decrypted_finish, decrypt(encrypted_finish));
+ td::TlParser parser(decrypted_finish);
+ auto message = e2e::e2e_HandshakePrivate::fetch(parser);
+ TRY_STATUS_PREFIX(parser.get_status(), "Failed to parse message: ");
+ if (message->get_id() != e2e::e2e_handshakePrivateFinish::ID) {
+ return td::Status::Error("Unexpected public message type");
+ }
+ auto finish = td::move_tl_object_as<e2e::e2e_handshakePrivateFinish>(message);
+ CHECK(finish);
+
+ if (finish->alice_nonce_ != alice_nonce_) {
+ return td::Status::Error("Bob's nonce mismatch");
+ }
+ if (finish->bob_nonce_ != bob_nonce_) {
+ return td::Status::Error("Bob's nonce mismatch");
+ }
+ if (PublicKey::from_u256(finish->alice_PK_) != alice_private_key_.to_public_key()) {
+ return td::Status::Error("Alice's public key mismatch");
+ }
+ if (PublicKey::from_u256(finish->bob_PK_) != bob_public_key_) {
+ return td::Status::Error("Bob's public key mismatch");
+ }
+ if (finish->alice_user_id_ != alice_user_id_) {
+ return td::Status::Error("Alice's user_id mismatch");
+ }
+ if (finish->bob_user_id_ != bob_user_id_) {
+ return td::Status::Error("Bob's user_id mismatch");
+ }
+
+ return td::Status::OK();
+}
+
+td::SecureString QRHandshakeAlice::encrypt_ephemeral(td::Slice data) const {
+ return MessageEncryption::encrypt_data(data, ephemeral_shared_secret_);
+}
+
+td::SecureString QRHandshakeAlice::encrypt(td::Slice data) const {
+ return MessageEncryption::encrypt_data(data, shared_secret_);
+}
+
+td::Result<td::SecureString> QRHandshakeAlice::decrypt(td::Slice data) const {
+ return MessageEncryption::decrypt_data(data, shared_secret_);
+}
+
+td::Result<td::SecureString> QRHandshakeAlice::shared_secret() const {
+ return td::SecureString(as_slice(ephemeral_shared_secret_));
+}
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/QRHandshake.h b/tde2e/td/e2e/QRHandshake.h
new file mode 100644
index 000000000..ce7f79cca
--- /dev/null
+++ b/tde2e/td/e2e/QRHandshake.h
@@ -0,0 +1,91 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include "td/e2e/utils.h"
+
+#include "td/utils/common.h"
+#include "td/utils/optional.h"
+#include "td/utils/SharedSlice.h"
+#include "td/utils/Slice.h"
+#include "td/utils/Status.h"
+#include "td/utils/UInt.h"
+
+#include <utility>
+
+namespace tde2e_core {
+
+struct QRHandshakeBob {
+ QRHandshakeBob(td::int64 bob_user_id, PrivateKey &&bob_private_key);
+
+ static QRHandshakeBob create(td::int64 bob_user_id, PrivateKey bob_private_key);
+
+ std::string generate_start() const;
+
+ td::Result<td::SecureString> receive_accept(td::int64 alice_user_id, PublicKey alice_public_key,
+ td::Slice encrypted_accept);
+
+ td::SecureString encrypt(td::Slice data) const;
+ td::Result<td::SecureString> decrypt(td::Slice encrypted_message) const;
+ td::Result<td::SecureString> decrypt_ephemeral(td::Slice encrypted_message) const;
+ td::Result<td::SecureString> shared_secret() const {
+ if (!o_ephemeral_shared_secret_) {
+ return td::Status::Error("No shared secret was set");
+ }
+ return td::SecureString(as_slice(*o_ephemeral_shared_secret_));
+ }
+
+ PrivateKey bob_ephemeral_private_key_;
+
+ PrivateKey bob_private_key_;
+ td::int64 bob_user_id_;
+ td::UInt256 bob_nonce_;
+
+ td::optional<td::int64> o_alice_user_id_;
+
+ td::optional<PublicKey> o_alice_public_key_;
+ td::optional<td::SecureString> o_shared_secret_;
+ td::optional<td::SecureString> o_ephemeral_shared_secret_;
+ td::optional<td::UInt256> o_alice_nonce_;
+
+ bool had_accept_{false};
+};
+
+struct QRHandshakeAlice {
+ QRHandshakeAlice(td::int64 alice_user_id, PrivateKey &&alice_private_key, td::int64 bob_user_id,
+ PublicKey &&bob_public_key, const td::UInt256 &bob_nonce, td::SecureString &&ephemeral_shared_secret,
+ td::SecureString &&shared_secret);
+
+ static td::Result<QRHandshakeAlice> create(td::int64 alice_user_id, PrivateKey alice_private_key,
+ td::int64 bob_user_id, PublicKey bob_public_key, td::Slice serialized_qr);
+
+ static td::string serialize_login_import(td::Slice accept, td::Slice encrypted_alice_pk);
+ static td::Result<std::pair<td::string, td::string>> deserialize_login_import(td::Slice data);
+ td::SecureString generate_accept() const;
+
+ td::Status receive_finish(td::Slice encrypted_finish);
+
+ td::SecureString encrypt_ephemeral(td::Slice data) const;
+ td::SecureString encrypt(td::Slice data) const;
+ td::Result<td::SecureString> decrypt(td::Slice data) const;
+ td::Result<td::SecureString> shared_secret() const;
+
+ PrivateKey alice_private_key_;
+ td::int64 alice_user_id_;
+ td::UInt256 alice_nonce_;
+
+ PublicKey bob_public_key_;
+ td::int64 bob_user_id_;
+ td::UInt256 bob_nonce_;
+
+ td::SecureString ephemeral_shared_secret_;
+ td::SecureString shared_secret_;
+
+ bool had_finish_{false};
+};
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/TestBlockchain.cpp b/tde2e/td/e2e/TestBlockchain.cpp
new file mode 100644
index 000000000..0c44ba8d3
--- /dev/null
+++ b/tde2e/td/e2e/TestBlockchain.cpp
@@ -0,0 +1,771 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/e2e/TestBlockchain.h"
+
+#include "td/utils/algorithm.h"
+#include "td/utils/base64.h"
+#include "td/utils/crypto.h"
+#include "td/utils/logging.h"
+#include "td/utils/misc.h"
+#include "td/utils/overloaded.h"
+#include "td/utils/simple_tests.h"
+#include "td/utils/SliceBuilder.h"
+
+#include <cstdio>
+#include <ctime>
+#include <set>
+#include <utility>
+
+int VERBOSITY_NAME(blkch) = VERBOSITY_NAME(INFO);
+
+namespace tde2e_api {
+Result<SecureBytes> call_export_shared_key(CallId call_id);
+} // namespace tde2e_api
+
+namespace tde2e_core {
+
+// BlockchainLogger implementation
+BlockchainLogger::BlockchainLogger(const std::string &log_file_path) : log_file_path_(log_file_path) {
+ log_file_.open(log_file_path, std::ios::out | std::ios::trunc); // Open in append mode
+ LOG(ERROR) << "OPENING BLOCKCHAIN LOG FILE: " << log_file_path_;
+ if (!log_file_.is_open()) {
+ LOG(ERROR) << "Failed to open blockchain log file: " << log_file_path;
+ } else {
+ // Write a header to indicate a new test session
+ log_file_ << "===== NEW TEST SESSION " << std::time(nullptr) << " =====\n";
+ log_file_.flush();
+ }
+}
+
+BlockchainLogger::~BlockchainLogger() {
+ close();
+}
+
+void BlockchainLogger::close() {
+ if (log_file_.is_open()) {
+ log_file_.close();
+ LOG(ERROR) << "CLOSE";
+ }
+}
+
+void BlockchainLogger::write_separator() {
+ log_file_ << "---\n";
+ log_file_.flush();
+}
+
+std::string BlockchainLogger::base64_encode(td::Slice data) {
+ return td::base64_encode(data);
+}
+
+void BlockchainLogger::log_try_apply_block(td::Slice block_slice, Height height, const td::Status &result) {
+ if (!log_file_.is_open())
+ return;
+
+ log_file_ << "TRY_APPLY_BLOCK\n";
+ log_file_ << base64_encode(block_slice) << "\n";
+ log_file_ << base64_encode(Blockchain::from_local_to_server(block_slice.str()).move_as_ok()) << "\n";
+ log_file_ << height.height << "\n";
+ log_file_ << height.broadcast_height << "\n";
+ if (result.is_ok()) {
+ log_file_ << "OK\n";
+ } else {
+ log_file_ << "ERROR " << result.code() << " " << result.message().str() << "\n";
+ }
+ write_separator();
+}
+
+void BlockchainLogger::log_try_apply_broadcast_block(td::Slice block_slice, Height height, const td::Status &result) {
+ if (!log_file_.is_open())
+ return;
+
+ log_file_ << "TRY_APPLY_BROADCAST_BLOCK\n";
+ log_file_ << base64_encode(block_slice) << "\n";
+ log_file_ << base64_encode(Blockchain::from_local_to_server(block_slice.str()).move_as_ok()) << "\n";
+ log_file_ << height.height << "\n";
+ log_file_ << height.broadcast_height << "\n";
+ if (result.is_ok()) {
+ log_file_ << "OK\n";
+ } else {
+ log_file_ << "ERROR " << result.code() << " " << result.message().str() << "\n";
+ }
+ write_separator();
+}
+
+void BlockchainLogger::log_reindex() {
+ if (!log_file_.is_open())
+ return;
+
+ log_file_ << "REINDEX\n";
+ write_separator();
+}
+
+void BlockchainLogger::log_reset() {
+ if (!log_file_.is_open())
+ return;
+
+ log_file_ << "RESET\n";
+ write_separator();
+}
+
+void BlockchainLogger::log_get_block(int subchain_id, size_t height, const td::Result<std::string> &result) {
+ if (!log_file_.is_open())
+ return;
+
+ log_file_ << "GET_BLOCK\n";
+ log_file_ << subchain_id << "\n";
+ log_file_ << height << "\n";
+ if (result.is_ok()) {
+ log_file_ << "OK\n";
+ log_file_ << base64_encode(result.ok()) << "\n";
+ } else {
+ log_file_ << "ERROR " << result.error().code() << " " << result.error().message().str() << "\n";
+ }
+ write_separator();
+}
+
+void BlockchainLogger::log_get_height(Height height) {
+ if (!log_file_.is_open())
+ return;
+
+ log_file_ << "GET_HEIGHT\n";
+ log_file_ << height.height << "\n";
+ log_file_ << height.broadcast_height << "\n";
+ write_separator();
+}
+
+void BlockchainLogger::log_get_proof(td::int64 height, const std::vector<std::string> &keys,
+ const td::Result<std::string> &result) {
+ if (!log_file_.is_open())
+ return;
+
+ log_file_ << "GET_PROOF\n";
+ log_file_ << height << "\n";
+ log_file_ << keys.size() << "\n";
+ for (const auto &key : keys) {
+ log_file_ << base64_encode(key) << "\n";
+ }
+ if (result.is_ok()) {
+ log_file_ << "OK\n";
+ log_file_ << base64_encode(result.ok()) << "\n";
+ } else {
+ log_file_ << "ERROR " << result.error().code() << " " << result.error().message().str() << "\n";
+ }
+ write_separator();
+}
+
+// ServerBlockchain implementation with logging
+td::Status ServerBlockchain::try_apply_block(td::Slice block_slice) {
+ TRY_RESULT(block, Block::from_tl_serialized(block_slice));
+ ValidateOptions validate_options;
+ validate_options.permissions = GroupParticipantFlags::AllPermissions;
+ validate_options.validate_signature = true;
+ validate_options.validate_state_hash = true;
+ auto status = blockchain_.try_apply_block(block, validate_options);
+ if (status.is_ok()) {
+ blocks_.push_back(block);
+ broadcast_chain_.on_new_main_block(blockchain_);
+ }
+
+ if (logger_) {
+ logger_->log_try_apply_block(block_slice, get_height(), status);
+ }
+
+ return status;
+}
+
+td::Status ServerBlockchain::try_apply_broadcast(td::Slice broadcast_slice) {
+ auto status = broadcast_chain_.try_apply_block(broadcast_slice);
+ if (status.is_ok()) {
+ broadcast_blocks_.push_back(broadcast_slice.str());
+ }
+ if (logger_) {
+ logger_->log_try_apply_broadcast_block(broadcast_slice, get_height(), status);
+ }
+ return status;
+}
+
+void ServerBlockchain::reindex() {
+ snapshot_ = blockchain_.state_.key_value_state_.build_snapshot().move_as_ok();
+ auto last_block = blockchain_.last_block_;
+ blockchain_ = Blockchain::create_from_block(last_block, snapshot_).move_as_ok();
+}
+
+td::Result<std::string> ServerBlockchain::get_block(size_t height, int sub_chain) const {
+ td::Result<std::string> result;
+ if (sub_chain == 0) {
+ if (height >= blocks_.size()) {
+ result = td::Status::Error(PSLICE() << "Invalid height " << height);
+ } else {
+ CHECK(blocks_[height].height_ == static_cast<td::int64>(height));
+ result = Blockchain::from_local_to_server(blocks_[height].to_tl_serialized());
+ }
+ } else if (sub_chain == 1) {
+ if (height >= broadcast_blocks_.size()) {
+ result = td::Status::Error(PSLICE() << "Invalid height " << height);
+ } else {
+ result = Blockchain::from_local_to_server(broadcast_blocks_[height]);
+ }
+ }
+
+ if (logger_) {
+ logger_->log_get_block(sub_chain, height, result);
+ }
+
+ return result;
+}
+
+Height ServerBlockchain::get_height() const {
+ auto height = blockchain_.get_height();
+ auto broadcast_height = static_cast<td::int64>(broadcast_blocks_.size()) - 1;
+ auto res = Height{height, broadcast_height};
+
+ /*
+ if (logger_) {
+ logger_->log_get_height(res);
+ }
+ */
+
+ return res;
+}
+
+td::Result<std::string> ServerBlockchain::get_proof(td::int64 height, const std::vector<std::string> &keys) const {
+ td::Result<std::string> result = [&]() -> td::Result<std::string> {
+ if (height != blockchain_.get_height()) {
+ return td::Status::Error("Invalid height");
+ }
+ auto keys_slices = td::transform(keys, [](auto &x) { return td::Slice(x); });
+ return blockchain_.state_.key_value_state_.gen_proof(keys_slices);
+ }();
+
+ if (logger_) {
+ logger_->log_get_proof(height, keys, result);
+ }
+
+ return result;
+}
+
+const Blockchain &ServerBlockchain::get_blockchain() const {
+ return blockchain_;
+}
+
+std::string BaselineBlockchainState::get_value(const std::string &key) const {
+ auto it = key_value_state.find(key);
+ if (it == key_value_state.end()) {
+ return "";
+ }
+ return it->second;
+}
+
+void BaselineBlockchainState::apply_changes(const std::vector<Change> &changes) {
+ for (const auto &change_v : changes) {
+ std::visit(td::overloaded([](const ChangeNoop &) {},
+ [&](const ChangeSetValue &change) { key_value_state[change.key] = change.value; },
+ [&](const ChangeSetGroupState &change) { group_state = change.group_state; },
+ [&](const ChangeSetSharedKey &change) { shared_key = change.shared_key; }),
+ change_v.value);
+ }
+ height++;
+}
+
+td::Status expect_error(tde2e_core::E expected_code, td::Result<ApplyResult> r_received) {
+ TRY_RESULT(received_result, std::move(r_received));
+ const auto &received = received_result.status;
+ auto expected_sw = tde2e_api::error_string(expected_code);
+ auto expected = td::Slice(expected_sw.data(), expected_sw.size());
+ if (received.is_ok()) {
+ return td::Status::Error(PSLICE() << "Unexpected OK, expected " << expected);
+ }
+ if (!td::begins_with(received.message(), expected)) {
+ return td::Status::Error(PSLICE() << "Unexpected " << received << ", expected " << expected);
+ }
+ return td::Status::OK();
+}
+
+Change BlockBuilder::make_group_change(const std::vector<GroupParticipant> &participants) {
+ return Change{ChangeSetGroupState{BlockBuilder::make_group_state(participants)}};
+}
+
+Change BlockBuilder::make_set_value(std::string key, std::string value) {
+ return Change{ChangeSetValue{std::move(key), std::move(value)}};
+}
+
+GroupSharedKeyRef BlockBuilder::make_shared_key(const std::vector<td::int64> &user_ids) {
+ auto n = user_ids.size();
+ auto res = std::make_shared<const GroupSharedKey>(
+ GroupSharedKey{PublicKey::from_u256({}), "dummy", user_ids, std::vector<std::string>(n, "??")});
+ if (user_ids.empty()) {
+ res = GroupSharedKey::empty_shared_key();
+ }
+ return res;
+}
+
+GroupStateRef BlockBuilder::make_group_state(std::vector<GroupParticipant> users, td::int32 external_permissions) {
+ return std::make_shared<GroupState>(GroupState{std::move(users), external_permissions});
+}
+
+Block BlockBuilder::finish() {
+ CHECK(has_height);
+ CHECK(has_signature);
+ CHECK(has_block_hash);
+ CHECK(has_hash_proof);
+ CHECK(has_shared_key_proof);
+ CHECK(has_group_state_proof);
+ CHECK(has_signature_public_key);
+ return block;
+}
+
+Block BlockBuilder::build(const PrivateKey &private_key) {
+ with_public_key(private_key.to_public_key());
+ sign(private_key);
+ return finish();
+}
+
+Block BlockBuilder::build_no_public_key(const PrivateKey &private_key) {
+ skip_public_key();
+ sign(private_key);
+ return finish();
+}
+
+Block BlockBuilder::build_zero_sign() {
+ zero_sign();
+ return finish();
+}
+
+BlockBuilder &BlockBuilder::with_height(td::int32 height) {
+ CHECK(!has_height);
+ has_height = true;
+ block.height_ = height;
+ return *this;
+}
+
+BlockBuilder &BlockBuilder::with_block_hash(td::UInt256 hash) {
+ CHECK(!has_block_hash);
+ has_block_hash = true;
+ block.prev_block_hash_ = hash;
+ return *this;
+}
+
+BlockBuilder &BlockBuilder::with_previous_block(Block &previous_block) {
+ return with_height(previous_block.height_ + 1).with_block_hash(previous_block.calc_hash());
+}
+
+BlockBuilder &BlockBuilder::with_public_key(const PrivateKey &private_key) {
+ return with_public_key(private_key.to_public_key());
+}
+
+BlockBuilder &BlockBuilder::with_public_key(const PublicKey &public_key) {
+ CHECK(!has_signature_public_key);
+ has_signature_public_key = true;
+ block.o_signature_public_key_ = public_key;
+ return *this;
+}
+
+BlockBuilder &BlockBuilder::skip_public_key() {
+ CHECK(!has_signature_public_key);
+ has_signature_public_key = true;
+ return *this;
+}
+
+BlockBuilder &BlockBuilder::set_value_raw(td::Slice key, td::Slice value) {
+ kv_state_.set_value(key, value).ensure();
+ block.state_proof_.kv_hash = KeyValueHash{kv_state_.get_hash()};
+ block.changes_.push_back(Change{ChangeSetValue{key.str(), value.str()}});
+ has_hash_proof = true;
+ return *this;
+}
+
+BlockBuilder &BlockBuilder::set_value(td::Slice key, td::Slice value) {
+ return set_value_raw(hash_key(key), std::move(value));
+}
+
+BlockBuilder &BlockBuilder::with_group_state(const std::vector<GroupParticipant> &users, bool in_changes, bool in_proof,
+ td::int32 external_permissions) {
+ auto state = make_group_state(users, external_permissions);
+ if (in_changes) {
+ block.changes_.push_back(Change{ChangeSetGroupState{state}});
+ }
+ if (in_proof) {
+ CHECK(!has_group_state_proof);
+ has_group_state_proof = true;
+ block.state_proof_.o_group_state = state;
+ }
+ return *this;
+}
+
+BlockBuilder &BlockBuilder::skip_group_state_proof() {
+ CHECK(!has_group_state_proof);
+ has_group_state_proof = true;
+ return *this;
+}
+
+BlockBuilder &BlockBuilder::with_shared_key(const std::vector<td::int64> &user_ids, bool in_changes, bool in_proof) {
+ auto shared_key = make_shared_key(user_ids);
+ if (in_changes) {
+ block.changes_.push_back(Change{ChangeSetSharedKey{shared_key}});
+ }
+ if (in_proof) {
+ CHECK(!has_shared_key_proof);
+ has_shared_key_proof = true;
+ block.state_proof_.o_shared_key = shared_key;
+ }
+ return *this;
+}
+
+BlockBuilder &BlockBuilder::skip_shared_key_proof() {
+ CHECK(!has_shared_key_proof);
+ has_shared_key_proof = true;
+ return *this;
+}
+
+void BlockBuilder::sign(const PrivateKey &private_key) {
+ if (!has_hash_proof) {
+ has_hash_proof = true;
+ block.state_proof_.kv_hash.hash = TrieNode::empty_node()->hash;
+ }
+
+ CHECK(!has_signature);
+ block.sign_inplace(private_key).ensure();
+ has_signature = true;
+}
+
+void BlockBuilder::zero_sign() {
+ if (!has_hash_proof) {
+ has_hash_proof = true;
+ block.state_proof_.kv_hash.hash = TrieNode::empty_node()->hash;
+ }
+
+ CHECK(!has_signature);
+ block.signature_ = {};
+ has_signature = true;
+}
+
+std::string BlockBuilder::hash_key(td::Slice key) const {
+ std::string hashed_key(32, 0);
+ td::sha256(key, hashed_key);
+ return hashed_key;
+}
+
+BlockchainTester::BlockchainTester() {
+ // Automatically set up logging
+ server_.set_logger(BlockchainLogger::get_instance());
+ BlockchainLogger::get_instance()->log_reset();
+}
+
+td::Result<ApplyResult> BlockchainTester::apply(const Block &block) {
+ return apply(block, block.to_tl_serialized());
+}
+
+td::Result<ApplyResult> BlockchainTester::apply(td::Slice block_str) {
+ TRY_RESULT(block, Block::from_tl_serialized(block_str));
+ return apply(block, block_str);
+}
+
+td::Result<ApplyResult> BlockchainTester::apply(const std::vector<Change> &changes, const PrivateKey &private_key) {
+ add_proof(changes);
+ auto r_block = client_.build_block(changes, private_key);
+ if (r_block.is_error()) {
+ return ApplyResult{r_block.move_as_error()};
+ }
+ return apply(r_block.move_as_ok());
+}
+
+td::Status BlockchainTester::expect_error(E expected, const Block &block) {
+ return ::tde2e_core::expect_error(expected, apply(block));
+}
+
+td::Status BlockchainTester::expect_error(E expected, td::Slice block) {
+ return ::tde2e_core::expect_error(expected, apply(block));
+}
+
+td::Status BlockchainTester::expect_ok(td::Slice block) {
+ TRY_RESULT(answer, apply(block));
+ return std::move(answer.status);
+}
+
+td::Status BlockchainTester::expect_ok_broadcast(td::Slice block) {
+ return server_.try_apply_broadcast(block);
+}
+
+td::Status BlockchainTester::expect_ok(const Block &block) {
+ TRY_RESULT(answer, apply(block));
+ return std::move(answer.status);
+}
+
+td::Status BlockchainTester::expect_ok(const std::vector<Change> &changes, const PrivateKey &private_key) {
+ TRY_RESULT(answer, apply(changes, private_key));
+ return std::move(answer.status);
+}
+
+td::Status BlockchainTester::expect_error(E expected, const std::vector<Change> &changes,
+ const PrivateKey &private_key) {
+ return ::tde2e_core::expect_error(expected, apply(changes, private_key));
+}
+
+void BlockchainTester::reindex() {
+ server_.reindex();
+}
+
+td::Result<std::vector<std::string>> BlockchainTester::get_values(const std::vector<std::string> &keys) {
+ add_proof(keys);
+ std::vector<std::string> values;
+ for (const auto &key : keys) {
+ auto client_value = client_.get_value(key).move_as_ok();
+ auto baseline_value = baseline_state_.get_value(key);
+ TEST_ASSERT_EQ(baseline_value, client_value, "baseline and client differs");
+ values.push_back(client_value);
+ }
+ return values;
+}
+
+td::Result<std::string> BlockchainTester::get_block_from_server(td::int64 height, int sub_chain) {
+ return server_.get_block(static_cast<std::size_t>(height), sub_chain);
+}
+
+td::Result<std::string> BlockchainTester::get_value(td::Slice key) {
+ TRY_RESULT(values, get_values({key.str()}));
+ return values.at(0);
+}
+
+td::Status BlockchainTester::expect_key_value(td::Slice key, td::Slice value) {
+ TEST_ASSERT_EQ(value, get_value(key), "");
+ return td::Status::OK();
+}
+
+void BlockchainTester::enable_logging(const std::string &log_file_path) {
+ //BlockchainLogger::set_log_file_path(log_file_path);
+ server_.set_logger(BlockchainLogger::get_instance());
+}
+
+td::Result<Height> BlockchainTester::get_height() {
+ return server_.get_height();
+}
+
+void BlockchainTester::add_proof(const std::vector<Change> &changes) {
+ std::vector<std::string> keys;
+ for (auto &change_v : changes) {
+ std::visit(
+ td::overloaded([](const ChangeNoop &) {}, [&keys](const ChangeSetValue &change) { keys.push_back(change.key); },
+ [](const ChangeSetGroupState &change) {}, [](const ChangeSetSharedKey &change) {}),
+ change_v.value);
+ }
+ add_proof(keys);
+}
+
+td::Result<ApplyResult> BlockchainTester::apply(const Block &block, td::Slice block_str) {
+ add_proof(block.changes_);
+ auto server_status = server_.try_apply_block(block_str);
+ auto client_status = client_.try_apply_block(block_str);
+ if (server_status.is_error() != client_status.is_error()) {
+ return td::Status::Error(PSLICE() << "Server and client return different answers:\n\tserver:" << server_status
+ << "\n\tclient:" << client_status);
+ }
+ if (server_status.is_error()) {
+ return ApplyResult{std::move(server_status)};
+ }
+ baseline_state_.apply_changes(block.changes_);
+ return ApplyResult{td::Status::OK()};
+}
+
+void BlockchainTester::add_proof(const std::vector<std::string> &keys) {
+ if (baseline_state_.height != -1) {
+ auto proof = server_.get_proof(baseline_state_.height, keys).move_as_ok();
+ client_.add_proof(proof).ensure();
+ }
+}
+
+CallTester::CallTester(int N) {
+ for (int i = 0; i < N; i++) {
+ auto key = tde2e_api::key_generate_temporary_private_key().value();
+ auto public_key = tde2e_api::key_from_public_key(tde2e_api::key_to_public_key(key).value()).value();
+ users.push_back(User{i + 1, key, public_key});
+ }
+}
+
+td::Status CallTester::start_call(const std::vector<int> &ids) {
+ auto call_state = make_state(ids);
+ TEST_TRY_RESULT(zero_block, to_td(tde2e_api::call_create_zero_block(users[ids[0]].private_key_id, call_state)));
+ for (auto id : ids) {
+ start_call(users[id]);
+ }
+ TEST_TRY_STATUS(bt.expect_ok(zero_block));
+ return td::Status::OK();
+}
+
+td::Status CallTester::update_call(int admin, const std::vector<int> &ids) {
+ auto call_state = make_state(ids);
+ CHECK(users[admin].call_id);
+ TEST_TRY_RESULT(block, to_td(tde2e_api::call_create_change_state_block(users[admin].call_id, call_state)));
+
+ std::set<int> s(ids.begin(), ids.end());
+ for (int i = 0; i < static_cast<int>(users.size()); i++) {
+ if (s.count(i) > 0) {
+ if (!users[i].call_id) {
+ start_call(users[i]);
+ }
+ } else if (users[i].call_id) {
+ stop_call(users[i]);
+ }
+ }
+ TEST_TRY_STATUS(bt.expect_ok(block));
+ return td::Status::OK();
+}
+
+td::Status CallTester::full_sync() {
+ for (auto &user : users) {
+ TEST_TRY_STATUS(user_full_sync(user));
+ }
+ return td::Status::OK();
+}
+
+td::Status CallTester::check_shared_key() {
+ TEST_TRY_STATUS(full_sync());
+ td::optional<std::string> o_key;
+ for (auto &user : users) {
+ if (!user.in_call) {
+ continue;
+ }
+ TRY_RESULT(key, to_td(tde2e_api::call_export_shared_key(user.call_id)));
+ if (!o_key) {
+ o_key = key;
+ }
+ TEST_ASSERT(!key.empty(), "key is empty");
+ TEST_ASSERT_EQ(o_key, key, "key differs");
+ }
+ return td::Status::OK();
+}
+
+td::Status CallTester::check_emoji_hash() {
+ TEST_TRY_STATUS(run_emoji_proto()); // ensure that proto is finished
+ td::optional<std::string> o_key;
+ for (auto &user : users) {
+ if (!user.in_call) {
+ continue;
+ }
+ TRY_RESULT(state, to_td(tde2e_api::call_get_verification_state(user.call_id)));
+ TEST_ASSERT(state.emoji_hash, "emoji hash is missing");
+ TEST_ASSERT(!state.emoji_hash->empty(), "emoji hash is empty");
+ if (!o_key) {
+ o_key = state.emoji_hash.value();
+ }
+ TEST_ASSERT_EQ(o_key, state.emoji_hash.value(), "key differs");
+ }
+ return td::Status::OK();
+}
+
+td::Status CallTester::run_emoji_proto() {
+ TEST_TRY_STATUS(full_send());
+ TEST_TRY_STATUS(full_sync());
+ TEST_TRY_STATUS(full_send());
+ TEST_TRY_STATUS(full_sync());
+ return td::Status::OK();
+}
+
+tde2e_api::CallParticipant CallTester::User::to_participant(int permissions) const {
+ return tde2e_api::CallParticipant{user_id, public_key_id, permissions};
+}
+
+void CallTester::start_call(User &user) {
+ CHECK(user.call_id == 0);
+ CHECK(!user.in_call);
+ user.in_call = true;
+ user.height = bt.get_height().move_as_ok();
+}
+
+void CallTester::stop_call(User &user) {
+ CHECK(user.call_id != 0);
+ CHECK(user.in_call);
+ user.in_call = false;
+ tde2e_api::call_destroy(user.call_id).value();
+ user.call_id = 0;
+}
+
+tde2e_api::CallState CallTester::make_state(const std::vector<int> &ids) {
+ tde2e_api::CallState state;
+ state.participants = td::transform(ids, [&](int uid) { return users[uid].to_participant(); });
+ return state;
+}
+
+td::Status CallTester::full_send() {
+ for (auto &user : users) {
+ TEST_TRY_STATUS(user_full_send(user));
+ }
+ return td::Status::OK();
+}
+
+td::Result<bool> CallTester::user_full_send(User &user) {
+ if (!user.call_id) {
+ return false;
+ }
+ TEST_TRY_RESULT(msgs, to_td(tde2e_api::call_pull_outbound_messages(user.call_id)));
+ if (msgs.empty()) {
+ return false;
+ }
+ TEST_ASSERT(msgs.size() == 1, "Wrong number of messages");
+ TEST_TRY_STATUS(bt.expect_ok_broadcast(msgs[0]));
+ return true;
+}
+
+td::Result<bool> CallTester::user_full_sync(User &user) {
+ if (!user.in_call) {
+ return false;
+ }
+ bool res = false;
+ if (!user.call_id) {
+ TEST_TRY_STATUS(user_init_call(user));
+ res = true;
+ }
+ while (true) {
+ TEST_TRY_RESULT(changed, user_sync_step(user));
+ if (!changed) {
+ return res;
+ }
+ res = true;
+ }
+}
+
+td::Status CallTester::user_init_call(User &user) {
+ CHECK(user.call_id == 0);
+ CHECK(user.in_call);
+ TEST_TRY_RESULT(block, bt.get_block_from_server(++user.height.height));
+ TRY_RESULT(call_id, to_td(tde2e_api::call_create(user.user_id, user.private_key_id, block)));
+ user.call_id = call_id;
+ return td::Status::OK();
+}
+
+td::Result<bool> CallTester::user_sync_step(User &user) {
+ TRY_RESULT(changed, user_sync_chain_step(user));
+ if (changed) {
+ return changed;
+ }
+ return user_sync_broadcast_step(user);
+}
+
+td::Result<bool> CallTester::user_sync_chain_step(User &user) {
+ auto height = bt.get_height().move_as_ok();
+ if (user.height.height == height.height) {
+ return false;
+ }
+ TEST_TRY_RESULT(block, bt.get_block_from_server(++user.height.height));
+ TEST_TRY_STATUS(to_td(tde2e_api::call_apply_block(user.call_id, block)));
+ return true;
+}
+
+td::Result<bool> CallTester::user_sync_broadcast_step(User &user) {
+ auto height = bt.get_height().move_as_ok();
+ CHECK(user.height.broadcast_height <= height.broadcast_height);
+ if (user.height.broadcast_height == height.broadcast_height) {
+ return false;
+ }
+ TEST_TRY_RESULT(block, bt.get_block_from_server(++user.height.broadcast_height, 1));
+ auto r = tde2e_api::call_receive_inbound_message(user.call_id, block);
+ if (!r.is_ok()) {
+ return td::Status::Error(PSLICE() << "Failed to call apply broadcast: " << r.error().message);
+ }
+ return true;
+}
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/TestBlockchain.h b/tde2e/td/e2e/TestBlockchain.h
new file mode 100644
index 000000000..64fa43707
--- /dev/null
+++ b/tde2e/td/e2e/TestBlockchain.h
@@ -0,0 +1,233 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include "td/e2e/Blockchain.h"
+#include "td/e2e/Call.h"
+#include "td/e2e/utils.h"
+
+#include "td/utils/common.h"
+#include "td/utils/logging.h"
+#include "td/utils/Slice.h"
+#include "td/utils/Status.h"
+#include "td/utils/UInt.h"
+
+#include <fstream>
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+// Define a custom verbosity name for blockchain-specific logging
+extern int VERBOSITY_NAME(blkch);
+
+namespace tde2e_core {
+
+struct Height {
+ td::int64 height;
+ td::int64 broadcast_height;
+};
+
+// Simple blockchain operation logger that writes to a file
+class BlockchainLogger {
+ public:
+ static std::shared_ptr<BlockchainLogger> &get_instance() {
+ static std::shared_ptr<BlockchainLogger> instance = std::make_shared<BlockchainLogger>("blockchain_test.log");
+ return instance;
+ }
+
+ explicit BlockchainLogger(const std::string &log_file_path);
+ ~BlockchainLogger();
+
+ void log_try_apply_block(td::Slice block_slice, Height height, const td::Status &result);
+ void log_try_apply_broadcast_block(td::Slice block_slice, Height height, const td::Status &result);
+ void log_reindex();
+ void log_reset();
+ void log_get_block(int subchain_id, size_t height, const td::Result<std::string> &result);
+ void log_get_height(Height height);
+ void log_get_proof(td::int64 height, const std::vector<std::string> &keys, const td::Result<std::string> &result);
+ void close();
+
+ private:
+ std::ofstream log_file_;
+ std::string log_file_path_;
+
+ void write_separator();
+ std::string base64_encode(td::Slice data);
+};
+
+class ServerBlockchain {
+ public:
+ ServerBlockchain() = default;
+ explicit ServerBlockchain(std::shared_ptr<BlockchainLogger> logger) : logger_(std::move(logger)) {
+ }
+
+ td::Status try_apply_block(td::Slice block_slice);
+ td::Status try_apply_broadcast(td::Slice broadcast_slice);
+ void reindex();
+ td::Result<std::string> get_block(size_t height, int sub_chain = 0) const;
+ Height get_height() const;
+ td::Result<std::string> get_proof(td::int64 height, const std::vector<std::string> &keys) const;
+ const Blockchain &get_blockchain() const;
+
+ void set_logger(std::shared_ptr<BlockchainLogger> logger) {
+ logger_ = std::move(logger);
+ }
+
+ private:
+ Blockchain blockchain_{Blockchain::create_empty()};
+ CallVerificationChain broadcast_chain_{};
+ std::vector<Block> blocks_;
+ std::vector<std::string> broadcast_blocks_;
+ std::string snapshot_;
+ std::shared_ptr<BlockchainLogger> logger_;
+};
+
+struct BaselineBlockchainState {
+ std::map<std::string, std::string> key_value_state;
+ GroupStateRef group_state;
+ GroupSharedKeyRef shared_key;
+ td::int32 height{-1};
+
+ std::string get_value(const std::string &key) const;
+ void apply_changes(const std::vector<Change> &changes);
+};
+
+struct ApplyResult {
+ td::Status status;
+};
+inline td::Status expect_error(tde2e_core::E expected_code, td::Result<ApplyResult> r_received);
+
+struct BlockBuilder {
+ static Change make_group_change(const std::vector<GroupParticipant> &participants);
+ static Change make_set_value(std::string key, std::string value);
+
+ static GroupSharedKeyRef make_shared_key(const std::vector<td::int64> &user_ids);
+ static GroupStateRef make_group_state(std::vector<GroupParticipant> users, td::int32 extrernal_permissions = 0);
+
+ Block finish();
+ Block build(const PrivateKey &private_key);
+ Block build_no_public_key(const PrivateKey &private_key);
+ Block build_zero_sign();
+
+ BlockBuilder &with_height(td::int32 height);
+ BlockBuilder &with_block_hash(td::UInt256 hash);
+ BlockBuilder &with_previous_block(Block &previous_block);
+
+ BlockBuilder &with_public_key(const PrivateKey &private_key);
+ BlockBuilder &with_public_key(const PublicKey &public_key);
+ BlockBuilder &skip_public_key();
+ BlockBuilder &set_value_raw(td::Slice key, td::Slice value);
+
+ //!!! we should check it fails with invalid key length!!!
+ BlockBuilder &set_value(td::Slice key, td::Slice value);
+ BlockBuilder &with_group_state(const std::vector<GroupParticipant> &users, bool in_changes = true,
+ bool in_proof = true, td::int32 external_permissions = 0);
+ BlockBuilder &skip_group_state_proof();
+ BlockBuilder &with_shared_key(const std::vector<td::int64> &user_ids, bool in_changes = true, bool in_proof = true);
+ BlockBuilder &skip_shared_key_proof();
+
+ private:
+ bool has_height{false};
+ bool has_block_hash{false};
+ bool has_hash_proof{false};
+ bool has_shared_key_proof{false};
+ bool has_group_state_proof{false};
+ bool has_signature_public_key{false};
+ bool has_signature{false};
+ tde2e_core::Block block;
+
+ KeyValueState kv_state_;
+
+ void sign(const PrivateKey &private_key);
+ void zero_sign();
+ std::string hash_key(td::Slice key) const;
+};
+
+struct BlockchainTester {
+ // Default constructor with automatic logging
+ BlockchainTester();
+
+ td::Result<ApplyResult> apply(const Block &block);
+ td::Result<ApplyResult> apply(td::Slice block_str);
+ td::Result<ApplyResult> apply(const std::vector<Change> &changes, const PrivateKey &private_key);
+ td::Status expect_error(E expected, const Block &block);
+ td::Status expect_error(E expected, td::Slice block);
+ td::Status expect_ok(td::Slice block);
+ td::Status expect_ok_broadcast(td::Slice block);
+ td::Status expect_ok(const Block &block);
+ td::Status expect_ok(const std::vector<Change> &changes, const PrivateKey &private_key);
+ td::Status expect_error(E expected, const std::vector<Change> &changes, const PrivateKey &private_key);
+ void reindex();
+ td::Result<std::vector<std::string>> get_values(const std::vector<std::string> &keys);
+
+ td::Result<std::string> get_block_from_server(td::int64 height, int sub_chain = 0);
+
+ td::Result<std::string> get_value(td::Slice key);
+
+ td::Status expect_key_value(td::Slice key, td::Slice value);
+
+ // For backwards compatibility
+ void enable_logging(const std::string &log_file_path);
+ td::Result<Height> get_height();
+
+ private:
+ BaselineBlockchainState baseline_state_;
+ ServerBlockchain server_;
+ ClientBlockchain client_;
+
+ void add_proof(const std::vector<Change> &changes);
+
+ td::Result<ApplyResult> apply(const Block &block, td::Slice block_str);
+
+ void add_proof(const std::vector<std::string> &keys);
+};
+
+struct CallTester {
+ explicit CallTester(int N = 10);
+
+ td::Status start_call(const std::vector<int> &ids);
+ td::Status update_call(int admin, const std::vector<int> &ids);
+ td::Status full_sync();
+ td::Status check_shared_key();
+ td::Status check_emoji_hash();
+
+ td::Status run_emoji_proto();
+
+ private:
+ struct User {
+ tde2e_api::UserId user_id;
+ tde2e_api::PrivateKeyId private_key_id;
+ tde2e_api::PublicKeyId public_key_id;
+ tde2e_api::CallId call_id{};
+
+ bool in_call{false};
+ Height height{};
+
+ tde2e_api::CallParticipant to_participant(int permissions = 3) const;
+ };
+
+ std::vector<User> users;
+ BlockchainTester bt;
+
+ void start_call(User &user);
+ void stop_call(User &user);
+ tde2e_api::CallState make_state(const std::vector<int> &ids);
+
+ td::Status full_send();
+
+ td::Result<bool> user_full_send(User &user);
+
+ td::Result<bool> user_full_sync(User &user);
+ td::Status user_init_call(User &user);
+
+ td::Result<bool> user_sync_step(User &user);
+ td::Result<bool> user_sync_chain_step(User &user);
+ td::Result<bool> user_sync_broadcast_step(User &user);
+};
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/Trie.cpp b/tde2e/td/e2e/Trie.cpp
new file mode 100644
index 000000000..912725e42
--- /dev/null
+++ b/tde2e/td/e2e/Trie.cpp
@@ -0,0 +1,506 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/e2e/Trie.h"
+
+#include "td/utils/algorithm.h"
+#include "td/utils/common.h"
+#include "td/utils/crypto.h"
+#include "td/utils/misc.h"
+#include "td/utils/Span.h"
+#include "td/utils/tl_helpers.h"
+#include "td/utils/tl_parsers.h"
+#include "td/utils/tl_storers.h"
+
+#include <iostream>
+#include <utility>
+
+namespace tde2e_core {
+
+TrieNode::TrieNode() : data(Empty{}) {
+ hash = compute_hash();
+}
+
+TrieNode::TrieNode(BitString key_suffix, std::string value) : data(Leaf{std::move(key_suffix), std::move(value)}) {
+ hash = compute_hash();
+}
+
+TrieNode::TrieNode(BitString prefix, TrieRef left, TrieRef right)
+ : data(Inner{std::move(prefix), std::move(left), std::move(right)}) {
+ hash = compute_hash();
+}
+
+TrieNode::TrieNode(const td::UInt256 &hash_value) : hash(hash_value), data(Pruned{-1, {}}) {
+}
+
+TrieNode::TrieNode(const td::UInt256 &hash_value, td::int64 offset, BitString base_bit_string)
+ : hash(hash_value), data(Pruned{offset, std::move(base_bit_string)}) {
+}
+
+TrieRef TrieNode::empty_node() {
+ static TrieRef node = std::make_shared<TrieNode>();
+ return node;
+}
+
+td::Result<TrieNode> fetch_node_from_snapshot(td::Slice, BitString &bs);
+
+td::Status TrieNode::try_load(td::Slice snapshot) const {
+ CHECK(get_type() == TrieNodeType::Pruned);
+ const auto &pruned = get_pruned();
+ if (pruned.offset < 0) {
+ return td::Status::Error("Cannot load pruned node");
+ }
+ if (td::narrow_cast<size_t>(pruned.offset) > snapshot.size()) {
+ return td::Status::Error("Cannot load pruned node: invalid offset");
+ }
+ auto bs = pruned.base_bit_string;
+ if (!bs.data_) {
+ bs = BitString(nullptr, bs.begin_bit_, bs.bits_size_);
+ }
+ TRY_RESULT(new_node, fetch_node_from_snapshot(snapshot.substr(td::narrow_cast<std::size_t>(pruned.offset)), bs));
+ if (new_node.hash != hash) {
+ return td::Status::Error("Cannot load pruned node: hash mismatch");
+ }
+ const_cast<TrieNode &>(*this) = std::move(new_node);
+ return td::Status::OK();
+}
+
+template <class StorerT>
+void store_for_hash(const TrieNode &node, StorerT &storer) {
+ using td::store;
+ auto type = node.get_type();
+ if (type == TrieNodeType::Leaf) {
+ store(type, storer);
+ auto &leaf = node.get_leaf();
+ store(leaf.key_suffix, storer);
+ store(leaf.value, storer);
+ } else if (type == TrieNodeType::Inner) {
+ store(type, storer);
+ auto &inner = node.get_inner();
+ store(inner.prefix, storer);
+ store(inner.left->hash, storer);
+ store(inner.right->hash, storer);
+ } else if (type == TrieNodeType::Empty) {
+ store(type, storer);
+ } else {
+ UNREACHABLE();
+ }
+}
+
+td::UInt256 TrieNode::compute_hash() const {
+ td::TlStorerCalcLength calc_length;
+ store_for_hash(*this, calc_length);
+ std::string buf(calc_length.get_length(), 0);
+ td::TlStorerUnsafe storer(td::MutableSlice(buf).ubegin());
+ store_for_hash(*this, storer);
+ td::UInt256 result_hash;
+ sha256(buf, result_hash.as_mutable_slice());
+ return result_hash;
+}
+
+td::Result<TrieRef> set(const TrieRef &n, BitString key, td::Slice value, td::Slice snapshot) {
+ CHECK(n);
+ auto type = n->get_type();
+
+ if (type == TrieNodeType::Pruned) {
+ TRY_STATUS(n->try_load(snapshot));
+ type = n->get_type();
+ CHECK(type != TrieNodeType::Pruned);
+ }
+
+ if (type == TrieNodeType::Empty) {
+ return std::make_shared<TrieNode>(std::move(key), value.str());
+ }
+
+ if (type == TrieNodeType::Leaf) {
+ const auto &leaf = n->get_leaf();
+ if (key == leaf.key_suffix) {
+ return std::make_shared<TrieNode>(key, value.str());
+ } else {
+ size_t i = key.common_prefix_length(leaf.key_suffix);
+ auto common_prefix = key.substr(0, i);
+
+ auto bit = key.get_bit(i);
+ auto left = std::make_shared<TrieNode>(key.substr(i + 1), value.str());
+ auto right = std::make_shared<TrieNode>(leaf.key_suffix.substr(i + 1), leaf.value);
+ if (bit) {
+ std::swap(left, right);
+ }
+ return std::make_shared<TrieNode>(std::move(common_prefix), std::move(left), std::move(right));
+ }
+ }
+
+ if (type == TrieNodeType::Inner) {
+ const auto &inner = n->get_inner();
+ size_t i = inner.prefix.common_prefix_length(key);
+
+ if (i < inner.prefix.bit_length()) {
+ auto common_prefix = inner.prefix.substr(0, i);
+ auto remaining_prefix = inner.prefix.substr(i + 1);
+ auto bit = inner.prefix.get_bit(i);
+
+ auto left = std::make_shared<TrieNode>(remaining_prefix, inner.left, inner.right);
+ auto right = std::make_shared<TrieNode>(key.substr(i + 1), value.str());
+
+ if (bit) {
+ std::swap(left, right);
+ }
+
+ return std::make_shared<TrieNode>(common_prefix, std::move(left), std::move(right));
+ } else {
+ auto key_bit = key.get_bit(i);
+ auto left = inner.left;
+ auto right = inner.right;
+ if (key_bit) {
+ TRY_RESULT_ASSIGN(right, set(right, key.substr(i + 1), value.str(), snapshot));
+ } else {
+ TRY_RESULT_ASSIGN(left, set(left, key.substr(i + 1), value.str(), snapshot));
+ }
+ return std::make_shared<TrieNode>(inner.prefix, std::move(left), std::move(right));
+ }
+ }
+
+ return nullptr;
+}
+
+td::Result<std::string> get(const TrieRef &n, const BitString &key, td::Slice snapshot) {
+ CHECK(n);
+ auto type = n->get_type();
+
+ if (type == TrieNodeType::Pruned) {
+ TRY_STATUS(n->try_load(snapshot));
+ type = n->get_type();
+ CHECK(type != TrieNodeType::Pruned);
+ }
+
+ if (type == TrieNodeType::Empty) {
+ return "";
+ }
+
+ if (type == TrieNodeType::Leaf) {
+ auto &leaf = n->get_leaf();
+ if (key == leaf.key_suffix) {
+ return leaf.value;
+ } else {
+ return "";
+ }
+ }
+
+ if (type == TrieNodeType::Inner) {
+ const auto &inner = n->get_inner();
+ auto prefix_length = inner.prefix.bit_length();
+ if (key.common_prefix_length(inner.prefix) != prefix_length) {
+ return "";
+ }
+
+ auto key_bit = key.get_bit(prefix_length);
+ if (key_bit) {
+ return get(inner.right, key.substr(prefix_length + 1), snapshot);
+ } else {
+ return get(inner.left, key.substr(prefix_length + 1), snapshot);
+ }
+ }
+
+ return "";
+}
+BitString to_key(td::Slice key) {
+ std::string buf;
+ if (key.size() != 32) {
+ buf.resize(32, 0);
+ td::MutableSlice(buf).copy_from(key);
+ key = buf;
+ }
+ return BitString(key);
+}
+
+td::Result<TrieRef> prune_node(const TrieRef &n, td::MutableSpan<BitString> keys, td::Slice snapshot) {
+ CHECK(n);
+ auto type = n->get_type();
+
+ if (type == TrieNodeType::Pruned) {
+ TRY_STATUS(n->try_load(snapshot));
+ type = n->get_type();
+ CHECK(type != TrieNodeType::Pruned);
+ }
+
+ if (type == TrieNodeType::Empty) {
+ return n;
+ }
+
+ if (keys.empty()) {
+ return std::make_shared<TrieNode>(n->hash);
+ }
+
+ if (type == TrieNodeType::Leaf) {
+ return n;
+ }
+
+ if (type == TrieNodeType::Inner) {
+ const auto &inner = n->get_inner();
+ std::vector<BitString> left_keys;
+ std::vector<BitString> right_keys;
+ for (const auto &key : keys) {
+ auto prefix_len = inner.prefix.bit_length();
+ if (key.common_prefix_length(inner.prefix) == prefix_len) {
+ if (key.get_bit(prefix_len)) {
+ right_keys.push_back(key.substr(prefix_len + 1));
+ } else {
+ left_keys.push_back(key.substr(prefix_len + 1));
+ }
+ }
+ }
+ TRY_RESULT(left, prune_node(inner.left, left_keys, snapshot));
+ TRY_RESULT(right, prune_node(inner.right, right_keys, snapshot));
+ return std::make_shared<TrieNode>(inner.prefix, std::move(left), std::move(right));
+ }
+ return n;
+}
+
+td::Result<TrieRef> generate_pruned_tree(const TrieRef &n, td::Span<td::Slice> keys, td::Slice snapshot) {
+ auto v = td::transform(keys, to_key);
+ return prune_node(n, v, snapshot);
+}
+
+std::ostream &operator<<(std::ostream &os, const td::UInt256 &hash) {
+ os << std::hex;
+ for (auto c : hash.raw) {
+ os << (c >> 4);
+ os << (c & 0xF);
+ }
+ os << std::dec; // Reset to decimal
+ return os;
+}
+
+void print_tree(const TrieRef &node, const std::string &prefix, bool is_root) {
+ if (!node) {
+ std::cout << prefix << "(null)\n";
+ return;
+ }
+
+ std::string type_str;
+ auto type = node->get_type();
+ switch (type) {
+ case TrieNodeType::Empty:
+ type_str = "Empty";
+ break;
+ case TrieNodeType::Leaf:
+ type_str = "Leaf";
+ break;
+ case TrieNodeType::Inner:
+ type_str = "Inner";
+ break;
+ case TrieNodeType::Pruned:
+ type_str = "Pruned";
+ break;
+ }
+
+ std::cout << prefix;
+ if (is_root) {
+ std::cout << "Root ";
+ }
+ std::cout << type_str << " Node, Hash: " << node->hash << "\n";
+
+ if (type == TrieNodeType::Leaf) {
+ const auto &leaf = node->get_leaf();
+ std::cout << prefix << " Key Suffix: " << leaf.key_suffix << "\n";
+ std::cout << prefix << " Value: " << leaf.value << "\n";
+ } else if (type == TrieNodeType::Inner) {
+ const auto &inner = node->get_inner();
+ std::cout << prefix << " Prefix: " << inner.prefix << "\n";
+ std::cout << prefix << " Children:\n";
+ std::string child_prefix = prefix + " ";
+ std::cout << prefix << " [0]\n";
+ print_tree(inner.left, child_prefix, false);
+ std::cout << prefix << " [1]\n";
+ print_tree(inner.right, child_prefix, false);
+ }
+}
+
+template <class StorerT>
+void store_for_network(const TrieNode &node, StorerT &storer) {
+ using td::store;
+ auto type = node.get_type();
+ store(type, storer);
+ if (type == TrieNodeType::Leaf) {
+ auto &leaf = node.get_leaf();
+ store(leaf.key_suffix, storer);
+ store(leaf.value, storer);
+ } else if (type == TrieNodeType::Inner) {
+ auto &inner = node.get_inner();
+ store(inner.prefix, storer);
+ store_for_network(*inner.left, storer);
+ store_for_network(*inner.right, storer);
+ } else if (type == TrieNodeType::Pruned) {
+ store(node.hash, storer);
+ } else if (type == TrieNodeType::Empty) {
+ } else {
+ UNREACHABLE();
+ }
+}
+
+template <class ParserT>
+void parse_from_network(TrieRef &ref, ParserT &parser) {
+ BitString bs(256);
+ parse_from_network(ref, parser, bs);
+}
+
+template <class ParserT>
+void parse_from_network(TrieRef &ref, ParserT &parser, BitString &bs) {
+ using td::parse;
+ TrieNodeType type;
+ parse(type, parser);
+ if (type == TrieNodeType::Leaf) {
+ BitString key_suffix = fetch_bit_string(parser, bs);
+ std::string value;
+ parse(value, parser);
+ ref = std::make_shared<TrieNode>(std::move(key_suffix), std::move(value));
+ } else if (type == TrieNodeType::Inner) {
+ BitString prefix = fetch_bit_string(parser, bs);
+ TrieRef left;
+ TrieRef right;
+
+ auto left_bs = bs.substr(prefix.bit_length() + 1);
+ parse_from_network(left, parser, left_bs);
+ auto right_bs = BitString(nullptr, left_bs.begin_bit_, left_bs.bit_length());
+ parse_from_network(right, parser, right_bs);
+ ref = std::make_shared<TrieNode>(std::move(prefix), std::move(left), std::move(right));
+ } else if (type == TrieNodeType::Pruned) {
+ td::UInt256 hash;
+ parse(hash, parser);
+ ref = std::make_shared<TrieNode>(std::move(hash));
+ } else if (type == TrieNodeType::Empty) {
+ ref = TrieNode::empty_node();
+ } else {
+ UNREACHABLE();
+ }
+}
+
+td::Result<std::string> TrieNode::serialize_for_network(const TrieRef &node) {
+ td::TlStorerCalcLength calc_length;
+ store_for_network(*node, calc_length);
+ std::string buf(calc_length.get_length(), 0);
+ td::TlStorerUnsafe storer(td::MutableSlice(buf).ubegin());
+ store_for_network(*node, storer);
+ return buf;
+}
+
+td::Result<TrieRef> TrieNode::fetch_from_network(td::Slice data) {
+ td::TlParser parser(data);
+ TrieRef res;
+ parse_from_network(res, parser);
+ parser.fetch_end();
+ TRY_STATUS(parser.get_status());
+ CHECK(res);
+ return res;
+}
+
+template <class StorerT, class F>
+td::Result<td::int64> store_for_snapshot(const TrieNode &node, StorerT &storer, const F &get_offset,
+ td::Slice snapshot) {
+ using td::store;
+ auto type = node.get_type();
+
+ if (type == TrieNodeType::Pruned) {
+ TRY_STATUS(node.try_load(snapshot));
+ type = node.get_type();
+ CHECK(type != TrieNodeType::Pruned);
+ }
+
+ if (type == TrieNodeType::Leaf) {
+ auto &leaf = node.get_leaf();
+ auto offset = get_offset();
+ store(type, storer);
+ store(leaf.key_suffix, storer);
+ store(leaf.value, storer);
+ return offset;
+ } else if (type == TrieNodeType::Inner) {
+ auto &inner = node.get_inner();
+ TRY_RESULT(left_offset, store_for_snapshot(*inner.left, storer, get_offset, snapshot));
+ TRY_RESULT(right_offset, store_for_snapshot(*inner.right, storer, get_offset, snapshot));
+ auto offset = get_offset();
+ store(type, storer);
+ store(inner.prefix, storer);
+ store(left_offset, storer);
+ store(inner.left->hash, storer);
+ store(right_offset, storer);
+ store(inner.right->hash, storer);
+ return offset;
+ } else if (type == TrieNodeType::Empty) {
+ auto offset = get_offset();
+ store(type, storer);
+ return offset;
+ } else {
+ UNREACHABLE();
+ }
+}
+
+td::Result<std::string> TrieNode::serialize_for_snapshot(const TrieRef &node, td::Slice snapshot) {
+ td::TlStorerCalcLength calc_length;
+ TRY_STATUS(store_for_snapshot(
+ *node, calc_length, [] { return td::int64{0}; }, snapshot));
+ std::string buf(calc_length.get_length() + 8, 0);
+ auto begin = td::MutableSlice(buf).ubegin();
+ td::TlStorerUnsafe storer(begin + 8);
+ auto get_offset = [&] {
+ return td::int64{storer.get_buf() - begin};
+ };
+ TRY_RESULT(root_offset, store_for_snapshot(*node, storer, get_offset, snapshot));
+ td::TlStorerUnsafe storer2(begin);
+ storer2.store_long(root_offset);
+ return buf;
+}
+
+td::Result<TrieNode> fetch_node_from_snapshot(td::Slice snapshot_slice, BitString &bs) {
+ td::TlParser parser(snapshot_slice);
+ using td::parse;
+ TrieNodeType type;
+ parse(type, parser);
+ if (type == TrieNodeType::Leaf) {
+ BitString key_suffix = fetch_bit_string(parser, bs);
+ std::string value;
+ parse(value, parser);
+ //parser.fetch_end();
+ TRY_STATUS(parser.get_status());
+ return TrieNode(std::move(key_suffix), std::move(value));
+ } else if (type == TrieNodeType::Inner) {
+ BitString prefix = fetch_bit_string(parser, bs);
+ td::int64 left_offset;
+ td::UInt256 left_hash;
+ parse(left_offset, parser);
+ parse(left_hash, parser);
+ td::int64 right_offset;
+ td::UInt256 right_hash;
+ parse(right_offset, parser);
+ parse(right_hash, parser);
+ //parser.fetch_end();
+ TRY_STATUS(parser.get_status());
+
+ auto left_bs = bs.substr(prefix.bit_length() + 1);
+ BitString right_bs;
+ right_bs.begin_bit_ = left_bs.begin_bit_;
+ right_bs.bits_size_ = left_bs.bits_size_;
+ auto left = std::make_shared<TrieNode>(left_hash, left_offset, std::move(left_bs));
+ auto right = std::make_shared<TrieNode>(right_hash, right_offset, std::move(right_bs));
+ return TrieNode(std::move(prefix), std::move(left), std::move(right));
+ } else if (type == TrieNodeType::Empty) {
+ return TrieNode();
+ }
+ return td::Status::Error("Failed to parse trie node");
+}
+
+td::Result<TrieRef> TrieNode::fetch_from_snapshot(td::Slice snapshot) {
+ td::TlParser parser(snapshot);
+ auto root_offset = static_cast<size_t>(parser.fetch_long());
+ TRY_STATUS(parser.get_status());
+ if (root_offset >= snapshot.size()) {
+ return td::Status::Error("Failed to parse");
+ }
+ auto bs = BitString(256);
+ TRY_RESULT(node, fetch_node_from_snapshot(snapshot.substr(root_offset), bs));
+ return std::make_shared<TrieNode>(std::move(node));
+}
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/Trie.h b/tde2e/td/e2e/Trie.h
new file mode 100644
index 000000000..b31900653
--- /dev/null
+++ b/tde2e/td/e2e/Trie.h
@@ -0,0 +1,102 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include "td/e2e/BitString.h"
+#include "td/e2e/utils.h"
+
+#include "td/utils/Slice.h"
+#include "td/utils/Span.h"
+#include "td/utils/Status.h"
+#include "td/utils/UInt.h"
+
+#include <memory>
+#include <tuple>
+#include <utility>
+#include <variant>
+
+namespace tde2e_core {
+
+enum class TrieNodeType : td::int32 { Empty, Leaf, Inner, Pruned };
+
+struct TrieNode;
+using TrieRef = std::shared_ptr<const TrieNode>;
+
+struct TrieNode {
+ td::UInt256 hash{};
+
+ struct Empty {};
+ struct Leaf {
+ BitString key_suffix;
+ std::string value;
+ };
+ struct Inner {
+ BitString prefix;
+ TrieRef left;
+ TrieRef right;
+ };
+ struct Pruned {
+ td::int64 offset;
+ BitString base_bit_string;
+ };
+ std::variant<Empty, Leaf, Inner, Pruned> data;
+
+ TrieNodeType get_type() const {
+ return static_cast<TrieNodeType>(data.index());
+ }
+
+ const Leaf &get_leaf() const {
+ return std::get<Leaf>(data);
+ }
+ const Inner &get_inner() const {
+ return std::get<Inner>(data);
+ }
+ const Pruned &get_pruned() const {
+ return std::get<Pruned>(data);
+ }
+
+ td::Status try_load(td::Slice snapshot) const;
+
+ TrieNode();
+ TrieNode(BitString key_suffix, std::string value);
+ TrieNode(BitString prefix, TrieRef left, TrieRef right);
+ explicit TrieNode(const td::UInt256 &hash_value);
+ TrieNode(const td::UInt256 &hash_value, td::int64 offset, BitString base_bit_string);
+ TrieNode(TrieNode &&) = default;
+ TrieNode &operator=(TrieNode &&) = default;
+ static TrieRef empty_node();
+
+ static td::Result<std::string> serialize_for_network(const TrieRef &node);
+ static td::Result<TrieRef> fetch_from_network(td::Slice data);
+ static td::Result<std::string> serialize_for_snapshot(const TrieRef &node, td::Slice snapshot);
+ static td::Result<TrieRef> fetch_from_snapshot(td::Slice snapshot);
+
+ private:
+ td::UInt256 compute_hash() const;
+};
+
+td::Result<TrieRef> set(const TrieRef &n, BitString key, td::Slice value, td::Slice snapshot = {});
+
+td::Result<std::string> get(const TrieRef &n, const BitString &key, td::Slice snapshot = {});
+
+td::Result<TrieRef> generate_pruned_tree(const TrieRef &n, td::Span<td::Slice> keys, td::Slice snapshot = {});
+
+std::ostream &operator<<(std::ostream &os, const td::UInt256 &hash);
+
+void print_tree(const TrieRef &node, const std::string &prefix = "", bool is_root = true);
+
+BitString to_key(td::Slice key);
+
+inline td::Result<TrieRef> set(const TrieRef &n, td::Slice key, td::Slice value) {
+ return set(n, to_key(key), value);
+}
+
+inline td::Result<std::string> get(const TrieRef &n, td::Slice key, td::Slice snapshot = {}) {
+ return get(n, to_key(key), snapshot);
+}
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/bip39.cpp b/tde2e/td/e2e/bip39.cpp
new file mode 100644
index 000000000..21f778a6b
--- /dev/null
+++ b/tde2e/td/e2e/bip39.cpp
@@ -0,0 +1,2063 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/e2e/bip39.h"
+
+namespace tde2e_core {
+
+td::CSlice bip39_english() {
+ return R"abcd(abandon
+ability
+able
+about
+above
+absent
+absorb
+abstract
+absurd
+abuse
+access
+accident
+account
+accuse
+achieve
+acid
+acoustic
+acquire
+across
+act
+action
+actor
+actress
+actual
+adapt
+add
+addict
+address
+adjust
+admit
+adult
+advance
+advice
+aerobic
+affair
+afford
+afraid
+again
+age
+agent
+agree
+ahead
+aim
+air
+airport
+aisle
+alarm
+album
+alcohol
+alert
+alien
+all
+alley
+allow
+almost
+alone
+alpha
+already
+also
+alter
+always
+amateur
+amazing
+among
+amount
+amused
+analyst
+anchor
+ancient
+anger
+angle
+angry
+animal
+ankle
+announce
+annual
+another
+answer
+antenna
+antique
+anxiety
+any
+apart
+apology
+appear
+apple
+approve
+april
+arch
+arctic
+area
+arena
+argue
+arm
+armed
+armor
+army
+around
+arrange
+arrest
+arrive
+arrow
+art
+artefact
+artist
+artwork
+ask
+aspect
+assault
+asset
+assist
+assume
+asthma
+athlete
+atom
+attack
+attend
+attitude
+attract
+auction
+audit
+august
+aunt
+author
+auto
+autumn
+average
+avocado
+avoid
+awake
+aware
+away
+awesome
+awful
+awkward
+axis
+baby
+bachelor
+bacon
+badge
+bag
+balance
+balcony
+ball
+bamboo
+banana
+banner
+bar
+barely
+bargain
+barrel
+base
+basic
+basket
+battle
+beach
+bean
+beauty
+because
+become
+beef
+before
+begin
+behave
+behind
+believe
+below
+belt
+bench
+benefit
+best
+betray
+better
+between
+beyond
+bicycle
+bid
+bike
+bind
+biology
+bird
+birth
+bitter
+black
+blade
+blame
+blanket
+blast
+bleak
+bless
+blind
+blood
+blossom
+blouse
+blue
+blur
+blush
+board
+boat
+body
+boil
+bomb
+bone
+bonus
+book
+boost
+border
+boring
+borrow
+boss
+bottom
+bounce
+box
+boy
+bracket
+brain
+brand
+brass
+brave
+bread
+breeze
+brick
+bridge
+brief
+bright
+bring
+brisk
+broccoli
+broken
+bronze
+broom
+brother
+brown
+brush
+bubble
+buddy
+budget
+buffalo
+build
+bulb
+bulk
+bullet
+bundle
+bunker
+burden
+burger
+burst
+bus
+business
+busy
+butter
+buyer
+buzz
+cabbage
+cabin
+cable
+cactus
+cage
+cake
+call
+calm
+camera
+camp
+can
+canal
+cancel
+candy
+cannon
+canoe
+canvas
+canyon
+capable
+capital
+captain
+car
+carbon
+card
+cargo
+carpet
+carry
+cart
+case
+cash
+casino
+castle
+casual
+cat
+catalog
+catch
+category
+cattle
+caught
+cause
+caution
+cave
+ceiling
+celery
+cement
+census
+century
+cereal
+certain
+chair
+chalk
+champion
+change
+chaos
+chapter
+charge
+chase
+chat
+cheap
+check
+cheese
+chef
+cherry
+chest
+chicken
+chief
+child
+chimney
+choice
+choose
+chronic
+chuckle
+chunk
+churn
+cigar
+cinnamon
+circle
+citizen
+city
+civil
+claim
+clap
+clarify
+claw
+clay
+clean
+clerk
+clever
+click
+client
+cliff
+climb
+clinic
+clip
+clock
+clog
+close
+cloth
+cloud
+clown
+club
+clump
+cluster
+clutch
+coach
+coast
+coconut
+code
+coffee
+coil
+coin
+collect
+color
+column
+combine
+come
+comfort
+comic
+common
+company
+concert
+conduct
+confirm
+congress
+connect
+consider
+control
+convince
+cook
+cool
+copper
+copy
+coral
+core
+corn
+correct
+cost
+cotton
+couch
+country
+couple
+course
+cousin
+cover
+coyote
+crack
+cradle
+craft
+cram
+crane
+crash
+crater
+crawl
+crazy
+cream
+credit
+creek
+crew
+cricket
+crime
+crisp
+critic
+crop
+cross
+crouch
+crowd
+crucial
+cruel
+cruise
+crumble
+crunch
+crush
+cry
+crystal
+cube
+culture
+cup
+cupboard
+curious
+current
+curtain
+curve
+cushion
+custom
+cute
+cycle
+dad
+damage
+damp
+dance
+danger
+daring
+dash
+daughter
+dawn
+day
+deal
+debate
+debris
+decade
+december
+decide
+decline
+decorate
+decrease
+deer
+defense
+define
+defy
+degree
+delay
+deliver
+demand
+demise
+denial
+dentist
+deny
+depart
+depend
+deposit
+depth
+deputy
+derive
+describe
+desert
+design
+desk
+despair
+destroy
+detail
+detect
+develop
+device
+devote
+diagram
+dial
+diamond
+diary
+dice
+diesel
+diet
+differ
+digital
+dignity
+dilemma
+dinner
+dinosaur
+direct
+dirt
+disagree
+discover
+disease
+dish
+dismiss
+disorder
+display
+distance
+divert
+divide
+divorce
+dizzy
+doctor
+document
+dog
+doll
+dolphin
+domain
+donate
+donkey
+donor
+door
+dose
+double
+dove
+draft
+dragon
+drama
+drastic
+draw
+dream
+dress
+drift
+drill
+drink
+drip
+drive
+drop
+drum
+dry
+duck
+dumb
+dune
+during
+dust
+dutch
+duty
+dwarf
+dynamic
+eager
+eagle
+early
+earn
+earth
+easily
+east
+easy
+echo
+ecology
+economy
+edge
+edit
+educate
+effort
+egg
+eight
+either
+elbow
+elder
+electric
+elegant
+element
+elephant
+elevator
+elite
+else
+embark
+embody
+embrace
+emerge
+emotion
+employ
+empower
+empty
+enable
+enact
+end
+endless
+endorse
+enemy
+energy
+enforce
+engage
+engine
+enhance
+enjoy
+enlist
+enough
+enrich
+enroll
+ensure
+enter
+entire
+entry
+envelope
+episode
+equal
+equip
+era
+erase
+erode
+erosion
+error
+erupt
+escape
+essay
+essence
+estate
+eternal
+ethics
+evidence
+evil
+evoke
+evolve
+exact
+example
+excess
+exchange
+excite
+exclude
+excuse
+execute
+exercise
+exhaust
+exhibit
+exile
+exist
+exit
+exotic
+expand
+expect
+expire
+explain
+expose
+express
+extend
+extra
+eye
+eyebrow
+fabric
+face
+faculty
+fade
+faint
+faith
+fall
+false
+fame
+family
+famous
+fan
+fancy
+fantasy
+farm
+fashion
+fat
+fatal
+father
+fatigue
+fault
+favorite
+feature
+february
+federal
+fee
+feed
+feel
+female
+fence
+festival
+fetch
+fever
+few
+fiber
+fiction
+field
+figure
+file
+film
+filter
+final
+find
+fine
+finger
+finish
+fire
+firm
+first
+fiscal
+fish
+fit
+fitness
+fix
+flag
+flame
+flash
+flat
+flavor
+flee
+flight
+flip
+float
+flock
+floor
+flower
+fluid
+flush
+fly
+foam
+focus
+fog
+foil
+fold
+follow
+food
+foot
+force
+forest
+forget
+fork
+fortune
+forum
+forward
+fossil
+foster
+found
+fox
+fragile
+frame
+frequent
+fresh
+friend
+fringe
+frog
+front
+frost
+frown
+frozen
+fruit
+fuel
+fun
+funny
+furnace
+fury
+future
+gadget
+gain
+galaxy
+gallery
+game
+gap
+garage
+garbage
+garden
+garlic
+garment
+gas
+gasp
+gate
+gather
+gauge
+gaze
+general
+genius
+genre
+gentle
+genuine
+gesture
+ghost
+giant
+gift
+giggle
+ginger
+giraffe
+girl
+give
+glad
+glance
+glare
+glass
+glide
+glimpse
+globe
+gloom
+glory
+glove
+glow
+glue
+goat
+goddess
+gold
+good
+goose
+gorilla
+gospel
+gossip
+govern
+gown
+grab
+grace
+grain
+grant
+grape
+grass
+gravity
+great
+green
+grid
+grief
+grit
+grocery
+group
+grow
+grunt
+guard
+guess
+guide
+guilt
+guitar
+gun
+gym
+habit
+hair
+half
+hammer
+hamster
+hand
+happy
+harbor
+hard
+harsh
+harvest
+hat
+have
+hawk
+hazard
+head
+health
+heart
+heavy
+hedgehog
+height
+hello
+helmet
+help
+hen
+hero
+hidden
+high
+hill
+hint
+hip
+hire
+history
+hobby
+hockey
+hold
+hole
+holiday
+hollow
+home
+honey
+hood
+hope
+horn
+horror
+horse
+hospital
+host
+hotel
+hour
+hover
+hub
+huge
+human
+humble
+humor
+hundred
+hungry
+hunt
+hurdle
+hurry
+hurt
+husband
+hybrid
+ice
+icon
+idea
+identify
+idle
+ignore
+ill
+illegal
+illness
+image
+imitate
+immense
+immune
+impact
+impose
+improve
+impulse
+inch
+include
+income
+increase
+index
+indicate
+indoor
+industry
+infant
+inflict
+inform
+inhale
+inherit
+initial
+inject
+injury
+inmate
+inner
+innocent
+input
+inquiry
+insane
+insect
+inside
+inspire
+install
+intact
+interest
+into
+invest
+invite
+involve
+iron
+island
+isolate
+issue
+item
+ivory
+jacket
+jaguar
+jar
+jazz
+jealous
+jeans
+jelly
+jewel
+job
+join
+joke
+journey
+joy
+judge
+juice
+jump
+jungle
+junior
+junk
+just
+kangaroo
+keen
+keep
+ketchup
+key
+kick
+kid
+kidney
+kind
+kingdom
+kiss
+kit
+kitchen
+kite
+kitten
+kiwi
+knee
+knife
+knock
+know
+lab
+label
+labor
+ladder
+lady
+lake
+lamp
+language
+laptop
+large
+later
+latin
+laugh
+laundry
+lava
+law
+lawn
+lawsuit
+layer
+lazy
+leader
+leaf
+learn
+leave
+lecture
+left
+leg
+legal
+legend
+leisure
+lemon
+lend
+length
+lens
+leopard
+lesson
+letter
+level
+liar
+liberty
+library
+license
+life
+lift
+light
+like
+limb
+limit
+link
+lion
+liquid
+list
+little
+live
+lizard
+load
+loan
+lobster
+local
+lock
+logic
+lonely
+long
+loop
+lottery
+loud
+lounge
+love
+loyal
+lucky
+luggage
+lumber
+lunar
+lunch
+luxury
+lyrics
+machine
+mad
+magic
+magnet
+maid
+mail
+main
+major
+make
+mammal
+man
+manage
+mandate
+mango
+mansion
+manual
+maple
+marble
+march
+margin
+marine
+market
+marriage
+mask
+mass
+master
+match
+material
+math
+matrix
+matter
+maximum
+maze
+meadow
+mean
+measure
+meat
+mechanic
+medal
+media
+melody
+melt
+member
+memory
+mention
+menu
+mercy
+merge
+merit
+merry
+mesh
+message
+metal
+method
+middle
+midnight
+milk
+million
+mimic
+mind
+minimum
+minor
+minute
+miracle
+mirror
+misery
+miss
+mistake
+mix
+mixed
+mixture
+mobile
+model
+modify
+mom
+moment
+monitor
+monkey
+monster
+month
+moon
+moral
+more
+morning
+mosquito
+mother
+motion
+motor
+mountain
+mouse
+move
+movie
+much
+muffin
+mule
+multiply
+muscle
+museum
+mushroom
+music
+must
+mutual
+myself
+mystery
+myth
+naive
+name
+napkin
+narrow
+nasty
+nation
+nature
+near
+neck
+need
+negative
+neglect
+neither
+nephew
+nerve
+nest
+net
+network
+neutral
+never
+news
+next
+nice
+night
+noble
+noise
+nominee
+noodle
+normal
+north
+nose
+notable
+note
+nothing
+notice
+novel
+now
+nuclear
+number
+nurse
+nut
+oak
+obey
+object
+oblige
+obscure
+observe
+obtain
+obvious
+occur
+ocean
+october
+odor
+off
+offer
+office
+often
+oil
+okay
+old
+olive
+olympic
+omit
+once
+one
+onion
+online
+only
+open
+opera
+opinion
+oppose
+option
+orange
+orbit
+orchard
+order
+ordinary
+organ
+orient
+original
+orphan
+ostrich
+other
+outdoor
+outer
+output
+outside
+oval
+oven
+over
+own
+owner
+oxygen
+oyster
+ozone
+pact
+paddle
+page
+pair
+palace
+palm
+panda
+panel
+panic
+panther
+paper
+parade
+parent
+park
+parrot
+party
+pass
+patch
+path
+patient
+patrol
+pattern
+pause
+pave
+payment
+peace
+peanut
+pear
+peasant
+pelican
+pen
+penalty
+pencil
+people
+pepper
+perfect
+permit
+person
+pet
+phone
+photo
+phrase
+physical
+piano
+picnic
+picture
+piece
+pig
+pigeon
+pill
+pilot
+pink
+pioneer
+pipe
+pistol
+pitch
+pizza
+place
+planet
+plastic
+plate
+play
+please
+pledge
+pluck
+plug
+plunge
+poem
+poet
+point
+polar
+pole
+police
+pond
+pony
+pool
+popular
+portion
+position
+possible
+post
+potato
+pottery
+poverty
+powder
+power
+practice
+praise
+predict
+prefer
+prepare
+present
+pretty
+prevent
+price
+pride
+primary
+print
+priority
+prison
+private
+prize
+problem
+process
+produce
+profit
+program
+project
+promote
+proof
+property
+prosper
+protect
+proud
+provide
+public
+pudding
+pull
+pulp
+pulse
+pumpkin
+punch
+pupil
+puppy
+purchase
+purity
+purpose
+purse
+push
+put
+puzzle
+pyramid
+quality
+quantum
+quarter
+question
+quick
+quit
+quiz
+quote
+rabbit
+raccoon
+race
+rack
+radar
+radio
+rail
+rain
+raise
+rally
+ramp
+ranch
+random
+range
+rapid
+rare
+rate
+rather
+raven
+raw
+razor
+ready
+real
+reason
+rebel
+rebuild
+recall
+receive
+recipe
+record
+recycle
+reduce
+reflect
+reform
+refuse
+region
+regret
+regular
+reject
+relax
+release
+relief
+rely
+remain
+remember
+remind
+remove
+render
+renew
+rent
+reopen
+repair
+repeat
+replace
+report
+require
+rescue
+resemble
+resist
+resource
+response
+result
+retire
+retreat
+return
+reunion
+reveal
+review
+reward
+rhythm
+rib
+ribbon
+rice
+rich
+ride
+ridge
+rifle
+right
+rigid
+ring
+riot
+ripple
+risk
+ritual
+rival
+river
+road
+roast
+robot
+robust
+rocket
+romance
+roof
+rookie
+room
+rose
+rotate
+rough
+round
+route
+royal
+rubber
+rude
+rug
+rule
+run
+runway
+rural
+sad
+saddle
+sadness
+safe
+sail
+salad
+salmon
+salon
+salt
+salute
+same
+sample
+sand
+satisfy
+satoshi
+sauce
+sausage
+save
+say
+scale
+scan
+scare
+scatter
+scene
+scheme
+school
+science
+scissors
+scorpion
+scout
+scrap
+screen
+script
+scrub
+sea
+search
+season
+seat
+second
+secret
+section
+security
+seed
+seek
+segment
+select
+sell
+seminar
+senior
+sense
+sentence
+series
+service
+session
+settle
+setup
+seven
+shadow
+shaft
+shallow
+share
+shed
+shell
+sheriff
+shield
+shift
+shine
+ship
+shiver
+shock
+shoe
+shoot
+shop
+short
+shoulder
+shove
+shrimp
+shrug
+shuffle
+shy
+sibling
+sick
+side
+siege
+sight
+sign
+silent
+silk
+silly
+silver
+similar
+simple
+since
+sing
+siren
+sister
+situate
+six
+size
+skate
+sketch
+ski
+skill
+skin
+skirt
+skull
+slab
+slam
+sleep
+slender
+slice
+slide
+slight
+slim
+slogan
+slot
+slow
+slush
+small
+smart
+smile
+smoke
+smooth
+snack
+snake
+snap
+sniff
+snow
+soap
+soccer
+social
+sock
+soda
+soft
+solar
+soldier
+solid
+solution
+solve
+someone
+song
+soon
+sorry
+sort
+soul
+sound
+soup
+source
+south
+space
+spare
+spatial
+spawn
+speak
+special
+speed
+spell
+spend
+sphere
+spice
+spider
+spike
+spin
+spirit
+split
+spoil
+sponsor
+spoon
+sport
+spot
+spray
+spread
+spring
+spy
+square
+squeeze
+squirrel
+stable
+stadium
+staff
+stage
+stairs
+stamp
+stand
+start
+state
+stay
+steak
+steel
+stem
+step
+stereo
+stick
+still
+sting
+stock
+stomach
+stone
+stool
+story
+stove
+strategy
+street
+strike
+strong
+struggle
+student
+stuff
+stumble
+style
+subject
+submit
+subway
+success
+such
+sudden
+suffer
+sugar
+suggest
+suit
+summer
+sun
+sunny
+sunset
+super
+supply
+supreme
+sure
+surface
+surge
+surprise
+surround
+survey
+suspect
+sustain
+swallow
+swamp
+swap
+swarm
+swear
+sweet
+swift
+swim
+swing
+switch
+sword
+symbol
+symptom
+syrup
+system
+table
+tackle
+tag
+tail
+talent
+talk
+tank
+tape
+target
+task
+taste
+tattoo
+taxi
+teach
+team
+tell
+ten
+tenant
+tennis
+tent
+term
+test
+text
+thank
+that
+theme
+then
+theory
+there
+they
+thing
+this
+thought
+three
+thrive
+throw
+thumb
+thunder
+ticket
+tide
+tiger
+tilt
+timber
+time
+tiny
+tip
+tired
+tissue
+title
+toast
+tobacco
+today
+toddler
+toe
+together
+toilet
+token
+tomato
+tomorrow
+tone
+tongue
+tonight
+tool
+tooth
+top
+topic
+topple
+torch
+tornado
+tortoise
+toss
+total
+tourist
+toward
+tower
+town
+toy
+track
+trade
+traffic
+tragic
+train
+transfer
+trap
+trash
+travel
+tray
+treat
+tree
+trend
+trial
+tribe
+trick
+trigger
+trim
+trip
+trophy
+trouble
+truck
+true
+truly
+trumpet
+trust
+truth
+try
+tube
+tuition
+tumble
+tuna
+tunnel
+turkey
+turn
+turtle
+twelve
+twenty
+twice
+twin
+twist
+two
+type
+typical
+ugly
+umbrella
+unable
+unaware
+uncle
+uncover
+under
+undo
+unfair
+unfold
+unhappy
+uniform
+unique
+unit
+universe
+unknown
+unlock
+until
+unusual
+unveil
+update
+upgrade
+uphold
+upon
+upper
+upset
+urban
+urge
+usage
+use
+used
+useful
+useless
+usual
+utility
+vacant
+vacuum
+vague
+valid
+valley
+valve
+van
+vanish
+vapor
+various
+vast
+vault
+vehicle
+velvet
+vendor
+venture
+venue
+verb
+verify
+version
+very
+vessel
+veteran
+viable
+vibrant
+vicious
+victory
+video
+view
+village
+vintage
+violin
+virtual
+virus
+visa
+visit
+visual
+vital
+vivid
+vocal
+voice
+void
+volcano
+volume
+vote
+voyage
+wage
+wagon
+wait
+walk
+wall
+walnut
+want
+warfare
+warm
+warrior
+wash
+wasp
+waste
+water
+wave
+way
+wealth
+weapon
+wear
+weasel
+weather
+web
+wedding
+weekend
+weird
+welcome
+west
+wet
+whale
+what
+wheat
+wheel
+when
+where
+whip
+whisper
+wide
+width
+wife
+wild
+will
+win
+window
+wine
+wing
+wink
+winner
+winter
+wire
+wisdom
+wise
+wish
+witness
+wolf
+woman
+wonder
+wood
+wool
+word
+work
+world
+worry
+worth
+wrap
+wreck
+wrestle
+wrist
+write
+wrong
+yard
+year
+yellow
+you
+young
+youth
+zebra
+zero
+zone
+zoo
+)abcd";
+}
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/bip39.h b/tde2e/td/e2e/bip39.h
new file mode 100644
index 000000000..3389fe5c3
--- /dev/null
+++ b/tde2e/td/e2e/bip39.h
@@ -0,0 +1,15 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include "td/utils/Slice.h"
+
+namespace tde2e_core {
+
+td::CSlice bip39_english();
+
+} // namespace tde2e_core
diff --git a/tde2e/td/e2e/e2e_api.cpp b/tde2e/td/e2e/e2e_api.cpp
new file mode 100644
index 000000000..f87aa05bd
--- /dev/null
+++ b/tde2e/td/e2e/e2e_api.cpp
@@ -0,0 +1,857 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/e2e/e2e_api.h"
+
+#include "td/e2e/Blockchain.h"
+#include "td/e2e/Call.h"
+#include "td/e2e/Container.h"
+#include "td/e2e/DecryptedKey.h"
+#include "td/e2e/EncryptedKey.h"
+#include "td/e2e/EncryptedStorage.h"
+#include "td/e2e/MessageEncryption.h"
+#include "td/e2e/Mnemonic.h"
+#include "td/e2e/QRHandshake.h"
+
+#include "td/utils/algorithm.h"
+#include "td/utils/base64.h"
+#include "td/utils/common.h"
+#include "td/utils/int_types.h"
+#include "td/utils/logging.h"
+#include "td/utils/overloaded.h"
+#include "td/utils/Random.h"
+#include "td/utils/SharedSlice.h"
+#include "td/utils/Slice.h"
+#include "td/utils/SliceBuilder.h"
+#include "td/utils/Span.h"
+#include "td/utils/Status.h"
+#include "td/utils/StringBuilder.h"
+#include "td/utils/tl_parsers.h"
+#include "td/utils/UInt.h"
+
+#include <memory>
+
+namespace tde2e_core {
+
+namespace api = tde2e_api;
+
+using SecretRef = SharedRef<td::SecureString>;
+using HandshakeBobRef = UniqueRef<QRHandshakeBob>;
+using HandshakeAliceRef = UniqueRef<QRHandshakeAlice>;
+using StorageRef = UniqueRef<EncryptedStorage>;
+using CallRef = UniqueRef<Call>;
+
+td::UInt256 to_hash(td::Slice tag, td::Slice serialization) {
+ auto res = MessageEncryption::hmac_sha512(tag, serialization);
+ td::UInt256 hash;
+ hash.as_mutable_slice().copy_from(res.as_slice().substr(0, 32));
+ return hash;
+}
+
+class KeyChain {
+ public:
+ KeyChain() {
+ set_log_verbosity_level(1).ignore();
+ }
+
+ td::Result<api::Ok> set_log_verbosity_level(td::int32 new_verbosity_level) {
+ if (0 <= new_verbosity_level && new_verbosity_level <= VERBOSITY_NAME(NEVER)) {
+ SET_VERBOSITY_LEVEL(VERBOSITY_NAME(FATAL) + new_verbosity_level);
+ return api::Ok{};
+ }
+ return td::Status::Error("Wrong new verbosity level specified");
+ }
+ td::Result<api::PrivateKeyId> generate_private_key() {
+ TRY_RESULT(mnemonic, Mnemonic::create_new({}));
+ return from_words(mnemonic.get_words_string());
+ }
+ td::Result<api::PrivateKeyId> generate_dummy_key() {
+ auto hash = to_hash("dummy key", "...");
+ return container_.try_build<Key>(hash, [&]() -> td::Result<PrivateKeyWithMnemonic> {
+ td::SecureString key(32, 1);
+ return PrivateKeyWithMnemonic::from_private_key(PrivateKey::from_slice(key).move_as_ok(), {});
+ });
+ }
+ td::Result<api::PrivateKeyId> generate_temporary_private_key() {
+ TRY_RESULT(private_key, PrivateKey::generate());
+ auto hash = to_hash("temporary private key", private_key.to_public_key().to_u256().as_slice());
+ return container_.try_build<Key>(hash, [&]() -> td::Result<PrivateKeyWithMnemonic> {
+ return PrivateKeyWithMnemonic::from_private_key(private_key, {});
+ });
+ }
+
+ td::Result<api::SymmetricKeyId> derive_secret(api::PrivateKeyId key_id, td::Slice tag) {
+ TRY_RESULT(pk, to_private_key_with_mnemonic(key_id));
+ auto hash = to_hash(PSLICE() << "derive secret with tag: " << td::base64_encode(tag),
+ pk.to_public_key().to_u256().as_slice());
+ return container_.try_build<Key>(hash, [&]() -> td::Result<td::SecureString> {
+ // TODO: this is probably wrong and should be changed
+ return MessageEncryption::hmac_sha512(pk.to_private_key().to_secure_string(), tag);
+ });
+ }
+
+ td::Result<api::PrivateKeyId> from_words(td::Slice words) {
+ auto hash = to_hash("private ed25519 key from menemonic", words);
+ return container_.try_build<Key>(hash, [&]() -> td::Result<PrivateKeyWithMnemonic> {
+ TRY_RESULT(mnemonic, Mnemonic::create(td::SecureString(words), td::SecureString("")));
+ TRY_RESULT(private_key, mnemonic_to_private_key(mnemonic));
+ return private_key;
+ });
+ }
+
+ td::Result<api::Bytes> to_encrypted_private_key(api::PrivateKeyId key_id, api::SymmetricKeyId secret_id) {
+ TRY_RESULT(pk, to_private_key_with_mnemonic(key_id));
+ TRY_RESULT(secret, to_secret_ref(secret_id));
+ auto decrypted_key =
+ DecryptedKey(td::transform(pk.words(), [](const auto &m) { return m.copy(); }), pk.to_private_key());
+ auto encrypted = decrypted_key.encrypt("tde2e private key", *secret);
+ return encrypted.encrypted_data.as_slice().str();
+ }
+
+ td::Result<api::PrivateKeyId> from_encrypted_private_key(td::Slice encrypted_private_key,
+ api::SymmetricKeyId secret_id) {
+ TRY_RESULT(secret, to_secret_ref(secret_id));
+ auto hash = to_hash(PSLICE() << "encrypted private ed25519 key " << encrypted_private_key.str(), *secret);
+ return container_.try_build<Key>(hash, [&]() -> td::Result<PrivateKeyWithMnemonic> {
+ // WOW. empty public key. is it good?
+ auto encrypted_key = EncryptedKey{td::SecureString(encrypted_private_key), {}, secret->copy()};
+ TRY_RESULT(decrypted_key, encrypted_key.decrypt("tde2e private key", false));
+ return PrivateKeyWithMnemonic::from_private_key(decrypted_key.private_key,
+ std::move(decrypted_key.mnemonic_words));
+ });
+ }
+
+ td::Result<api::Bytes> to_encrypted_private_key_internal(api::PrivateKeyId key_id, api::SymmetricKeyId secret_id) {
+ TRY_RESULT(pk, to_private_key_with_mnemonic(key_id));
+ TRY_RESULT(secret, to_secret_ref(secret_id));
+ return MessageEncryption::encrypt_data(pk.to_private_key().to_secure_string(), *secret).as_slice().str();
+ }
+
+ td::Result<api::PrivateKeyId> from_encrypted_private_key_internal(td::Slice encrypted_private_key,
+ api::SymmetricKeyId secret_id) {
+ TRY_RESULT(secret, to_secret_ref(secret_id));
+ auto hash = to_hash(PSLICE() << "encrypted private ed25519 key internal " << encrypted_private_key.str(), *secret);
+ return container_.try_build<Key>(hash, [&]() -> td::Result<PrivateKeyWithMnemonic> {
+ TRY_RESULT(raw_pk, MessageEncryption::decrypt_data(encrypted_private_key, *secret));
+ TRY_RESULT(pk, PrivateKey::from_slice(raw_pk));
+ return PrivateKeyWithMnemonic::from_private_key(pk, {});
+ });
+ }
+
+ td::Result<api::PublicKeyId> from_public_key(td::Slice public_key) {
+ TRY_RESULT(key, PublicKey::from_slice(public_key));
+ auto hash = to_hash("public ed25519 key", public_key);
+ return container_.try_build<Key>(hash, [&]() -> td::Result<PublicKey> { return std::move(key); });
+ }
+
+ td::Result<api::SymmetricKeyId> from_ecdh(api::PrivateKeyId private_key_id, api::PublicKeyId public_key_id) {
+ TRY_RESULT(public_key, to_public_key(public_key_id));
+ TRY_RESULT(private_key, to_private_key_with_mnemonic(private_key_id));
+ auto hash = to_hash("x25519 shared secret",
+ public_key.to_u256().as_slice().str() + private_key.to_public_key().to_u256().as_slice().str());
+ return container_.try_build<Key>(hash, [&]() -> td::Result<td::SecureString> {
+ TRY_RESULT(shared_secret, private_key.to_private_key().compute_shared_secret(public_key));
+ return std::move(shared_secret);
+ });
+ }
+
+ td::Result<api::SymmetricKeyId> from_bytes(td::Slice secret) {
+ auto hash = to_hash("raw secret", secret);
+ return container_.try_build<Key>(hash, [&]() -> td::Result<td::SecureString> { return td::SecureString(secret); });
+ }
+ td::Result<api::SecureBytes> to_words(api::PrivateKeyId private_key_id) {
+ TRY_RESULT(private_key, to_private_key_with_mnemonic(private_key_id));
+ api::SecureBytes res;
+ auto words = private_key.words();
+ for (size_t i = 0; i < words.size(); ++i) {
+ if (i != 0) {
+ res += ' ';
+ }
+ res.append(words[i].data(), words[i].size());
+ }
+ return res;
+ }
+
+ td::Result<api::Int512> sign(api::PrivateKeyId key, td::Slice data) {
+ TRY_RESULT(private_key_ref, to_private_key_with_mnemonic(key));
+ TRY_RESULT(signature, private_key_ref.sign(td::Slice(data.data(), data.size())));
+ CHECK(signature.to_slice().size() == 64);
+ api::Int512 result;
+ td::MutableSlice(result.data(), result.size()).copy_from(signature.to_slice());
+ return result;
+ }
+
+ td::Status destroy(std::optional<api::AnyKeyId> o_key_id) {
+ return container_.destroy<Key>(o_key_id);
+ }
+
+ td::Result<api::EncryptedMessageForMany> encrypt_message_for_many(const std::vector<api::SymmetricKeyId> &key_ids,
+ td::Slice message) {
+ std::vector<SecretRef> secrets;
+ for (auto &key_id : key_ids) {
+ TRY_RESULT(secret, to_secret_ref(key_id));
+ secrets.emplace_back(std::move(secret));
+ }
+
+ td::SecureString one_time_secret(32);
+ td::Random::secure_bytes(one_time_secret.as_mutable_slice());
+ api::EncryptedMessageForMany res;
+ res.encrypted_message = MessageEncryption::encrypt_data(message, one_time_secret).as_slice().str();
+ for (auto &secret : secrets) {
+ TRY_RESULT(encrypted_header,
+ MessageEncryption::encrypt_header(one_time_secret, res.encrypted_message, secret->as_slice()));
+ res.encrypted_headers.emplace_back(encrypted_header.as_slice().str());
+ }
+ return res;
+ }
+ td::Result<api::EncryptedMessageForMany> re_encrypt_message_for_many(api::SymmetricKeyId decrypt_key,
+ const std::vector<api::SymmetricKeyId> &key_ids,
+ td::Slice encrypted_header,
+ td::Slice encrypted_message) {
+ std::vector<SecretRef> secrets;
+ for (auto &key_id : key_ids) {
+ TRY_RESULT(secret, to_secret_ref(key_id));
+ secrets.emplace_back(std::move(secret));
+ }
+ TRY_RESULT(secret_ref, to_secret_ref(decrypt_key));
+ TRY_RESULT(header, MessageEncryption::decrypt_header(encrypted_header, encrypted_message, secret_ref->as_slice()));
+
+ api::EncryptedMessageForMany res;
+ for (auto &secret : secrets) {
+ TRY_RESULT(new_encrypted_header,
+ MessageEncryption::encrypt_header(header, secret->as_slice(), encrypted_message));
+ res.encrypted_headers.emplace_back(new_encrypted_header.as_slice().str());
+ }
+ return res;
+ }
+
+ td::Result<api::SecureBytes> decrypt_message_for_many(api::SymmetricKeyId key_id, td::Slice encrypted_header,
+ td::Slice encrypted_message) {
+ TRY_RESULT(secret, to_secret_ref(key_id));
+ TRY_RESULT(header, MessageEncryption::decrypt_header(encrypted_header, encrypted_message, secret->as_slice()));
+ TRY_RESULT(message, MessageEncryption::decrypt_data(encrypted_message, header));
+ return message.as_slice().str();
+ }
+
+ td::Result<api::SecureBytes> encrypt_message_for_one(api::SymmetricKeyId key_id, td::Slice message) {
+ TRY_RESULT(secret, to_secret_ref(key_id));
+ auto encrypted_message = MessageEncryption::encrypt_data(message, secret->as_slice());
+ return encrypted_message.as_slice().str();
+ }
+
+ td::Result<api::SecureBytes> decrypt_message_for_one(api::SymmetricKeyId key_id, td::Slice encrypted_message) {
+ TRY_RESULT(secret, to_secret_ref(key_id));
+ TRY_RESULT(message, MessageEncryption::decrypt_data(encrypted_message, secret->as_slice()));
+ return message.as_slice().str();
+ }
+
+ td::Result<api::HandshakeId> handshake_create_for_bob(api::UserId bob_user_id, api::PrivateKeyId bob_private_key_id) {
+ TRY_RESULT(private_key_ref, to_private_key_with_mnemonic(bob_private_key_id));
+ return container_.try_build<Handshake>({}, [&]() -> td::Result<QRHandshakeBob> {
+ return QRHandshakeBob::create(bob_user_id, private_key_ref.to_private_key());
+ });
+ }
+ td::Result<api::Bytes> handshake_bob_send_start(api::HandshakeId bob_handshake_id) {
+ TRY_RESULT(bob_handshake, to_handshake_bob_ref(bob_handshake_id));
+ return bob_handshake->generate_start();
+ }
+ td::Result<api::HandshakeId> handshake_create_for_alice(api::UserId alice_user_id,
+ api::PrivateKeyId alice_private_key_id,
+ api::UserId bob_user_id, td::Slice bob_public_key,
+ td::Slice start) {
+ TRY_RESULT(private_key_ref, to_private_key_with_mnemonic(alice_private_key_id));
+ TRY_RESULT(bob_public_key_internal, PublicKey::from_slice(bob_public_key));
+ return container_.try_build<Handshake>({}, [&] {
+ return QRHandshakeAlice::create(alice_user_id, private_key_ref.to_private_key(), bob_user_id,
+ bob_public_key_internal, start.str());
+ });
+ }
+ td::Result<api::Bytes> handshake_alice_send_accept(api::HandshakeId alice_handshake_id) {
+ TRY_RESULT(alice_handshake, to_handshake_alice_ref(alice_handshake_id));
+ return alice_handshake->generate_accept().as_slice().str();
+ }
+
+ td::Result<api::Bytes> handshake_bob_receive_accept_send_finish(api::HandshakeId bob_handshake_id,
+ api::UserId alice_id, td::Slice alice_public_key,
+ td::Slice accept) {
+ TRY_RESULT(bob_handshake, to_handshake_bob_ref(bob_handshake_id));
+ TRY_RESULT(alice_public_key_internal, PublicKey::from_slice(alice_public_key));
+ TRY_RESULT(msg, bob_handshake->receive_accept(alice_id, alice_public_key_internal, accept.str()));
+ return msg.as_slice().str();
+ }
+
+ td::Result<api::Ok> handshake_alice_receive_finish(api::HandshakeId alice_handshake_id, td::Slice finish) {
+ TRY_RESULT(alice_handshake, to_handshake_alice_ref(alice_handshake_id));
+ TRY_STATUS(alice_handshake->receive_finish(finish));
+ return api::Ok();
+ }
+
+ td::Result<api::SymmetricKeyId> handshake_get_shared_key_id(api::HandshakeId handshake_id) {
+ TRY_RESULT(handshake, container_.get_unique<Handshake>(handshake_id));
+ TRY_RESULT(shared_secret, std::visit([&](auto &&v) { return v.shared_secret(); }, *handshake));
+ auto hash = to_hash("handshake shared_secret", shared_secret.as_slice());
+ return container_.try_build<Key>(hash, [&]() -> td::Result<td::SecureString> { return std::move(shared_secret); });
+ }
+
+ td::Result<api::Ok> handshake_destroy(std::optional<api::HandshakeId> o_handshake_id) {
+ TRY_STATUS(container_.destroy<Handshake>(o_handshake_id));
+ return api::Ok();
+ }
+
+ td::Result<api::Bytes> handshake_get_start_id(td::Slice start) {
+ auto hash = to_hash("handshake start id", start);
+ return hash.as_slice().str();
+ }
+ td::Result<api::LoginId> login_create_for_bob() {
+ auto bob_fake_id = 0;
+ auto bob_fake_pk = generate_dummy_key().move_as_ok();
+ return handshake_create_for_bob(bob_fake_id, bob_fake_pk);
+ }
+ td::Result<api::Bytes> login_bob_send_start(api::LoginId bob_login_id) {
+ TRY_RESULT(bob_handshake, to_handshake_bob_ref(bob_login_id));
+ return bob_handshake->generate_start();
+ }
+ td::Result<api::Bytes> login_create_for_alice(api::UserId alice_user_id, api::PrivateKeyId alice_private_key_id,
+ td::Slice start) {
+ auto bob_fake_id = 0;
+ auto bob_fake_pk = generate_dummy_key().move_as_ok();
+ TRY_RESULT(handshake_id,
+ handshake_create_for_alice(alice_user_id, alice_private_key_id, bob_fake_id,
+ to_public_key(bob_fake_pk).move_as_ok().to_secure_string(), start));
+ TRY_RESULT(shared_key_id, handshake_get_shared_key_id(handshake_id));
+ TRY_RESULT(encrypted_alice_pk, to_encrypted_private_key(alice_private_key_id, shared_key_id));
+ TRY_RESULT(accept, handshake_alice_send_accept(handshake_id));
+ return QRHandshakeAlice::serialize_login_import(accept, encrypted_alice_pk);
+ }
+
+ td::Result<api::PrivateKeyId> login_finish_for_bob(api::LoginId bob_login_id, api::UserId alice_user_id,
+ const api::PublicKey &alice_public_key, td::Slice data) {
+ std::pair<std::string, std::string> accept_and_key;
+ {
+ TRY_RESULT(bob_handshake, to_handshake_bob_ref(bob_login_id));
+ TRY_RESULT_ASSIGN(accept_and_key, QRHandshakeAlice::deserialize_login_import(data));
+ TRY_RESULT(alice_public_key_internal, PublicKey::from_slice(alice_public_key));
+ TRY_RESULT(finish, bob_handshake->receive_accept(alice_user_id, alice_public_key_internal, accept_and_key.first));
+ }
+ TRY_RESULT(shared_key_id, handshake_get_shared_key_id(bob_login_id));
+ return from_encrypted_private_key(accept_and_key.second, shared_key_id);
+ }
+
+ api::Result<api::Ok> login_destroy(api::LoginId login_id) {
+ return handshake_destroy(login_id);
+ }
+ td::Result<api::Ok> login_destroy_all() {
+ return handshake_destroy({});
+ }
+ td::Result<api::StorageId> storage_create(api::PrivateKeyId key_id, td::Slice last_block) {
+ TRY_RESULT(private_key_ref, to_private_key_with_mnemonic(key_id));
+
+ TRY_RESULT(storage, EncryptedStorage::create(last_block, private_key_ref.to_private_key()));
+ return container_.emplace<EncryptedStorage>(std::move(storage));
+ }
+
+ td::Result<api::Ok> storage_destroy(std::optional<api::StorageId> o_storage_id) {
+ TRY_STATUS(container_.destroy<EncryptedStorage>(o_storage_id));
+ return api::Ok();
+ }
+
+ td::Result<api::Ok> call_destroy(std::optional<api::CallId> o_call_id) {
+ TRY_STATUS(container_.destroy<Call>(o_call_id));
+ return api::Ok();
+ }
+
+ template <class T>
+ td::Result<api::UpdateId> storage_update_contact(api::StorageId storage_id, api::PublicKeyId key,
+ api::SignedEntry<T> signed_entry) {
+ TRY_RESULT(storage_ref, to_storage_ref(storage_id));
+ TRY_RESULT(public_key_ref, to_public_key(key));
+ return storage_ref->update(KeyContactByPublicKey{public_key_ref.to_u256()}, std::move(signed_entry));
+ }
+ template <class T>
+ td::Result<api::SignedEntry<T>> storage_sign_entry(api::PrivateKeyId key, api::Entry<T> entry) {
+ TRY_RESULT(private_key_ref, to_private_key_with_mnemonic(key));
+ return EncryptedStorage::sign_entry(private_key_ref.to_private_key(), std::move(entry));
+ }
+ td::Result<std::optional<api::Contact>> storage_get_contact(api::StorageId storage_id, api::PublicKeyId key) {
+ TRY_RESULT(storage_ref, to_storage_ref(storage_id));
+ TRY_RESULT(public_key_ref, to_public_key(key));
+ return storage_ref->get(KeyContactByPublicKey{public_key_ref.to_u256()}, false);
+ }
+ td::Result<std::optional<api::Contact>> storage_get_contact_optimistic(api::StorageId storage_id,
+ api::PublicKeyId key) {
+ TRY_RESULT(storage_ref, to_storage_ref(storage_id));
+ TRY_RESULT(public_key_ref, to_public_key(key));
+ return storage_ref->get(KeyContactByPublicKey{public_key_ref.to_u256()}, true);
+ }
+ td::Result<std::int64_t> storage_blockchain_height(api::StorageId storage_id) {
+ TRY_RESULT(storage_ref, to_storage_ref(storage_id));
+ return storage_ref->get_height();
+ }
+ td::Result<api::StorageUpdates> storage_blockchain_apply_block(api::StorageId storage_id, td::Slice block) {
+ TRY_RESULT(storage_ref, to_storage_ref(storage_id));
+ TRY_RESULT(updates, storage_ref->apply_block(block));
+ auto fixed_updates = td::transform(updates.updates, [&](auto update) {
+ auto public_key_id = from_public_key(update.first.public_key.as_slice()).move_as_ok();
+ return std::make_pair(public_key_id, std::move(update.second));
+ });
+ return api::StorageUpdates{std::move(fixed_updates)};
+ }
+ td::Result<api::Ok> storage_blockchain_add_proof(api::StorageId storage_id, td::Slice proof,
+ td::Span<std::string> keys) {
+ TRY_RESULT(storage_ref, to_storage_ref(storage_id));
+ TRY_STATUS(storage_ref->add_proof(proof, keys));
+ return api::Ok();
+ }
+ td::Result<api::StorageBlockchainState> storage_get_blockchain_state(api::StorageId storage_id) {
+ TRY_RESULT(storage_ref, to_storage_ref(storage_id));
+ auto state = storage_ref->get_blockchain_state();
+ return api::StorageBlockchainState{state.next_block, state.need_proofs};
+ }
+
+ td::Result<GroupStateRef> to_group_state(const api::CallState &call_state) {
+ GroupState group_state;
+ group_state.external_permissions = GroupParticipantFlags::AddUsers | GroupParticipantFlags::RemoveUsers;
+ for (auto &participant : call_state.participants) {
+ TRY_RESULT(public_key, to_public_key(participant.public_key_id));
+ group_state.participants.push_back(
+ GroupParticipant{participant.user_id, participant.permissions & 3, public_key, 0});
+ }
+ return std::make_shared<GroupState>(std::move(group_state));
+ }
+ td::Result<api::CallState> to_call_state(const GroupState &group_state) {
+ api::CallState call_state;
+ for (auto &participant : group_state.participants) {
+ auto public_key_id = from_public_key(participant.public_key.to_secure_string()).move_as_ok();
+ call_state.participants.push_back(
+ api::CallParticipant{participant.user_id, public_key_id, participant.flags & 3});
+ }
+ return call_state;
+ }
+
+ td::Result<api::Bytes> call_create_zero_block(api::PrivateKeyId private_key_id, const api::CallState &initial_state) {
+ TRY_RESULT(private_key_ref, to_private_key_with_mnemonic(private_key_id));
+ TRY_RESULT(group_state, to_group_state(initial_state));
+ return Call::create_zero_block(private_key_ref.to_private_key(), group_state);
+ }
+ tde2e_api::Result<std::string> call_create_self_add_block(api::PrivateKeyId private_key_id, td::Slice previous_block,
+ const tde2e_api::CallParticipant &self) {
+ TRY_RESULT(private_key_ref, to_private_key_with_mnemonic(private_key_id));
+ TRY_RESULT(public_key, to_public_key(self.public_key_id));
+ return Call::create_self_add_block(private_key_ref.to_private_key(), previous_block,
+ tde2e_core::GroupParticipant{self.user_id, 3, public_key, 0});
+ }
+
+ td::Result<api::CallId> call_create(api::UserId user_id, api::PrivateKeyId private_key_id, td::Slice last_block) {
+ TRY_RESULT(private_key_ref, to_private_key_with_mnemonic(private_key_id));
+
+ TRY_RESULT(call, Call::create(user_id, private_key_ref.to_private_key(), last_block));
+ return container_.emplace<Call>(std::move(call));
+ }
+ td::Result<api::Bytes> call_describe(api::CallId call_id) {
+ TRY_RESULT(call_ref, to_call_ref(call_id));
+ td::StringBuilder sb;
+ sb << *call_ref;
+ return sb.as_cslice().str();
+ }
+
+ td::Result<api::Bytes> call_create_change_state_block(api::CallId call_id, const api::CallState &new_state) {
+ TRY_RESULT(call_ref, to_call_ref(call_id));
+ TRY_RESULT(group_state, to_group_state(new_state));
+ return call_ref->build_change_state(group_state);
+ }
+ td::Result<api::SecureBytes> call_export_shared_key(api::CallId call_id) {
+ TRY_RESULT(call_ref, to_call_ref(call_id));
+ TRY_RESULT(shared_key, call_ref->shared_key());
+ return shared_key.as_slice().str();
+ }
+ td::Result<api::Bytes> call_encrypt(api::CallId call_id, api::CallChannelId channel_id, td::Slice message) {
+ TRY_RESULT(call_ref, to_call_ref(call_id));
+ return call_ref->encrypt(channel_id, message);
+ }
+ td::Result<api::SecureBytes> call_decrypt(api::CallId call_id, api::UserId user_id, api::CallChannelId channel_id,
+ td::Slice message) {
+ TRY_RESULT(call_ref, to_call_ref(call_id));
+ return call_ref->decrypt(user_id, channel_id, message);
+ }
+
+ td::Result<int> call_get_height(api::CallId call_id) {
+ TRY_RESULT(call_ref, to_call_ref(call_id));
+ return call_ref->get_height();
+ }
+ td::Result<api::CallState> call_apply_block(api::CallId call_id, td::Slice block) {
+ TRY_RESULT(call_ref, to_call_ref(call_id));
+ TRY_STATUS(call_ref->apply_block(block));
+ TRY_RESULT(group_state, call_ref->get_group_state());
+ return to_call_state(*group_state);
+ }
+
+ td::Result<api::CallState> call_get_state(api::CallId call_id) {
+ TRY_RESULT(call_ref, to_call_ref(call_id));
+ TRY_RESULT(group_state, call_ref->get_group_state());
+ return to_call_state(*group_state);
+ }
+
+ td::Result<api::CallVerificationState> call_get_verification_state(api::CallId call_id) {
+ TRY_RESULT(call_ref, to_call_ref(call_id));
+ return call_ref->get_verification_state();
+ }
+ td::Result<api::CallVerificationState> call_receive_inbound_message(api::CallId call_id, td::Slice message) {
+ TRY_RESULT(call_ref, to_call_ref(call_id));
+ return call_ref->receive_inbound_message(message);
+ }
+ td::Result<std::vector<std::string>> call_pull_outbound_messages(api::CallId call_id) {
+ TRY_RESULT(call_ref, to_call_ref(call_id));
+ return call_ref->pull_outbound_messages();
+ }
+
+ td::Result<api::CallVerificationWords> call_get_verification_words(api::CallId call_id) {
+ TRY_RESULT(call_ref, to_call_ref(call_id));
+ return call_ref->get_verification_words();
+ }
+ td::Result<api::PublicKey> to_public_key_api(api::AnyKeyId key_id) const {
+ TRY_RESULT(public_key, to_public_key(key_id));
+ return public_key.to_secure_string().as_slice().str();
+ }
+
+ private:
+ using Key = std::variant<td::SecureString, PublicKey, PrivateKeyWithMnemonic>;
+ using Handshake = std::variant<QRHandshakeAlice, QRHandshakeBob>;
+ Container<TypeInfo<Key, false, true>, TypeInfo<Handshake, true, true>, TypeInfo<EncryptedStorage, true, false>,
+ TypeInfo<Call, true, true>>
+ container_;
+
+ td::Result<PrivateKeyWithMnemonic> mnemonic_to_private_key(const Mnemonic &mnemonic) {
+ auto decrypted_key = DecryptedKey(mnemonic);
+ auto private_key = PrivateKeyWithMnemonic::from_private_key(mnemonic.to_private_key(), mnemonic.get_words());
+ return private_key;
+ }
+
+ td::Result<PublicKey> to_public_key(api::AnyKeyId key_id) const {
+ TRY_RESULT(key, container_.get_shared<Key>(key_id));
+ return std::visit(
+ td::overloaded([&](const PrivateKeyWithMnemonic &pk) -> td::Result<PublicKey> { return pk.to_public_key(); },
+ [&](const PublicKey &pk) -> td::Result<PublicKey> { return pk; },
+ [](const auto &) -> td::Result<PublicKey> {
+ return td::Status::Error(static_cast<int>(api::ErrorCode::InvalidInput),
+ "key_id doesn't contain public key");
+ }),
+ *key);
+ }
+
+ td::Result<PrivateKeyWithMnemonic> to_private_key_with_mnemonic(api::AnyKeyId key_id) const {
+ TRY_RESULT(key, container_.get_shared<Key>(key_id));
+ TRY_RESULT(ref, convert<PrivateKeyWithMnemonic>(std::move(key)));
+ return *ref;
+ }
+
+ td::Result<SecretRef> to_secret_ref(api::AnyKeyId key_id) const {
+ TRY_RESULT(key, container_.get_shared<Key>(key_id));
+ return convert<td::SecureString>(std::move(key));
+ }
+
+ td::Result<HandshakeAliceRef> to_handshake_alice_ref(api::HandshakeId alice_handshake_id) {
+ TRY_RESULT(handshake, container_.get_unique<Handshake>(alice_handshake_id));
+ return convert<QRHandshakeAlice>(std::move(handshake));
+ }
+ td::Result<HandshakeBobRef> to_handshake_bob_ref(api::HandshakeId bob_handshake_id) {
+ TRY_RESULT(handshake, container_.get_unique<Handshake>(bob_handshake_id));
+ return convert<QRHandshakeBob>(std::move(handshake));
+ }
+ td::Result<StorageRef> to_storage_ref(api::StorageId storage_id) {
+ return container_.get_unique<EncryptedStorage>(storage_id);
+ }
+ td::Result<CallRef> to_call_ref(api::CallId call_id) {
+ return container_.get_unique<Call>(call_id);
+ }
+};
+
+} // namespace tde2e_core
+namespace tde2e_api {
+tde2e_core::KeyChain &get_default_keychain() {
+ static tde2e_core::KeyChain keychain;
+ return keychain;
+}
+td::Slice to_slice(std::string_view s) {
+ if (s.empty()) {
+ return td::Slice();
+ }
+ return td::Slice(s.data(), s.size());
+}
+Result<Ok> set_log_verbosity_level(int new_verbosity_level) {
+ return get_default_keychain().set_log_verbosity_level(new_verbosity_level);
+}
+Result<PrivateKeyId> key_generate_private_key() {
+ return get_default_keychain().generate_private_key();
+}
+Result<PrivateKeyId> key_generate_temporary_private_key() {
+ return get_default_keychain().generate_temporary_private_key();
+}
+Result<PrivateKeyId> key_derive_secret(PrivateKeyId key_id, Slice tag) {
+ return get_default_keychain().derive_secret(key_id, to_slice(tag));
+}
+Result<Bytes> key_to_encrypted_private_key(PrivateKeyId key_id, SymmetricKeyId secret_id) {
+ return get_default_keychain().to_encrypted_private_key(key_id, secret_id);
+}
+Result<PrivateKeyId> key_from_encrypted_private_key(Slice encrypted_key, SymmetricKeyId secret_id) {
+ return get_default_keychain().from_encrypted_private_key(to_slice(encrypted_key), secret_id);
+}
+Result<SymmetricKeyId> key_from_bytes(SecureSlice secret) {
+ return get_default_keychain().from_bytes(to_slice(secret));
+}
+Result<Bytes> key_to_encrypted_private_key_internal(PrivateKeyId key_id, SymmetricKeyId secret_id) {
+ return get_default_keychain().to_encrypted_private_key_internal(key_id, secret_id);
+}
+Result<PrivateKeyId> key_from_encrypted_private_key_internal(Slice encrypted_key, SymmetricKeyId secret_id) {
+ return get_default_keychain().from_encrypted_private_key_internal(to_slice(encrypted_key), secret_id);
+}
+
+Result<PublicKeyId> key_from_public_key(Slice public_key) {
+ return get_default_keychain().from_public_key(to_slice(public_key));
+}
+
+Result<PrivateKeyId> key_from_ecdh(PrivateKeyId key_id, PublicKeyId other_public_key_id) {
+ return get_default_keychain().from_ecdh(key_id, other_public_key_id);
+}
+
+Result<PublicKey> key_to_public_key(PrivateKeyId key_id) {
+ return get_default_keychain().to_public_key_api(key_id);
+}
+
+Result<SecureBytes> key_to_words(PrivateKeyId key_id) {
+ return get_default_keychain().to_words(key_id);
+}
+Result<PrivateKeyId> key_from_words(SecureSlice words) {
+ return get_default_keychain().from_words(to_slice(words));
+}
+Result<Int512> key_sign(PrivateKeyId key, Slice data) {
+ return get_default_keychain().sign(key, to_slice(data));
+}
+Result<Ok> key_destroy(AnyKeyId key_id) {
+ TRY_STATUS(get_default_keychain().destroy(key_id));
+ return Ok();
+}
+Result<Ok> key_destroy_all() {
+ TRY_STATUS(get_default_keychain().destroy({}));
+ return Ok();
+}
+
+Result<EncryptedMessageForMany> encrypt_message_for_many(const std::vector<SymmetricKeyId> &key_ids,
+ SecureSlice message) {
+ return get_default_keychain().encrypt_message_for_many(std::move(key_ids), to_slice(message));
+}
+Result<SecureBytes> decrypt_message_for_many(SymmetricKeyId key_id, Slice encrypted_header, Slice encrypted_message) {
+ return get_default_keychain().decrypt_message_for_many(key_id, to_slice(encrypted_header),
+ to_slice(encrypted_message));
+}
+Result<Bytes> encrypt_message_for_one(SymmetricKeyId key_id, SecureSlice message) {
+ return get_default_keychain().encrypt_message_for_one(key_id, to_slice(message));
+}
+Result<SecureBytes> decrypt_message_for_one(SymmetricKeyId key_id, Slice encrypted_message) {
+ return get_default_keychain().decrypt_message_for_one(key_id, to_slice(encrypted_message));
+}
+Result<EncryptedMessageForMany> re_encrypt_message_for_many(SymmetricKeyId decrypt_key_id,
+ const std::vector<SymmetricKeyId> &encrypt_key_ids,
+ Slice encrypted_header, Slice encrypted_message) {
+ return get_default_keychain().re_encrypt_message_for_many(decrypt_key_id, std::move(encrypt_key_ids),
+ to_slice(encrypted_header), to_slice(encrypted_message));
+}
+
+Result<HandshakeId> handshake_create_for_bob(UserId bob_user_id, PrivateKeyId bob_private_key_id) {
+ return get_default_keychain().handshake_create_for_bob(bob_user_id, bob_private_key_id);
+}
+Result<Bytes> handshake_bob_send_start(HandshakeId bob_handshake_id) {
+ return get_default_keychain().handshake_bob_send_start(bob_handshake_id);
+}
+Result<HandshakeId> handshake_create_for_alice(UserId alice_user_id, PrivateKeyId alice_private_key_id,
+ UserId bob_user_id, const PublicKey &bob_public_key, Slice start) {
+ return get_default_keychain().handshake_create_for_alice(alice_user_id, alice_private_key_id, bob_user_id,
+ to_slice(bob_public_key), to_slice(start));
+}
+Result<Bytes> handshake_alice_send_accept(HandshakeId alice_handshake_id) {
+ return get_default_keychain().handshake_alice_send_accept(alice_handshake_id);
+}
+Result<Bytes> handshake_bob_receive_accept_send_finish(HandshakeId bob_handshake_id, UserId alice_id,
+ const PublicKey &alice_public_key, Slice accept) {
+ return get_default_keychain().handshake_bob_receive_accept_send_finish(bob_handshake_id, alice_id,
+ to_slice(alice_public_key), to_slice(accept));
+}
+Result<Bytes> handshake_start_id(Slice start) {
+ return get_default_keychain().handshake_get_start_id(to_slice(start));
+}
+Result<Ok> handshake_alice_receive_finish(HandshakeId alice_handshake_id, Slice finish) {
+ return get_default_keychain().handshake_alice_receive_finish(alice_handshake_id, to_slice(finish));
+}
+Result<SymmetricKeyId> handshake_get_shared_key_id(HandshakeId handshake_id) {
+ return get_default_keychain().handshake_get_shared_key_id(handshake_id);
+}
+Result<Ok> handshake_destroy(HandshakeId handshake_id) {
+ return get_default_keychain().handshake_destroy(handshake_id);
+}
+Result<Ok> handshake_destroy_all() {
+ return get_default_keychain().handshake_destroy({});
+}
+
+Result<LoginId> login_create_for_bob() {
+ return get_default_keychain().login_create_for_bob();
+}
+Result<Bytes> login_bob_send_start(LoginId bob_login_id) {
+ return get_default_keychain().login_bob_send_start(bob_login_id);
+}
+Result<Bytes> login_create_for_alice(UserId alice_user_id, PrivateKeyId alice_private_key_id, Slice start) {
+ return get_default_keychain().login_create_for_alice(alice_user_id, alice_private_key_id, to_slice(start));
+}
+Result<PrivateKeyId> login_finish_for_bob(LoginId bob_login_id, UserId alice_user_id, const PublicKey &alice_public_key,
+ Slice data) {
+ return get_default_keychain().login_finish_for_bob(bob_login_id, alice_user_id, alice_public_key, to_slice(data));
+}
+Result<Ok> login_destroy(LoginId login_id) {
+ return get_default_keychain().login_destroy(login_id);
+}
+Result<Ok> login_destroy_all() {
+ return get_default_keychain().login_destroy_all();
+}
+
+Result<StorageId> storage_create(PrivateKeyId key_id, Slice last_block) {
+ return get_default_keychain().storage_create(key_id, to_slice(last_block));
+}
+Result<Ok> storage_destroy(StorageId storage_id) {
+ return get_default_keychain().storage_destroy(storage_id);
+}
+Result<Ok> storage_destroy_all() {
+ return get_default_keychain().storage_destroy({});
+}
+template <class T>
+Result<UpdateId> storage_update_contact(StorageId storage_id, PublicKeyId key, SignedEntry<T> signed_entry) {
+ return get_default_keychain().storage_update_contact(storage_id, key, std::move(signed_entry));
+}
+template <class T>
+Result<SignedEntry<T>> storage_sign_entry(PrivateKeyId key, Entry<T> entry) {
+ return get_default_keychain().storage_sign_entry(key, std::move(entry));
+}
+Result<std::optional<Contact>> storage_get_contact(StorageId storage_id, PublicKeyId key) {
+ return get_default_keychain().storage_get_contact(storage_id, key);
+}
+Result<std::optional<Contact>> storage_get_contact_optimistic(StorageId storage_id, PublicKeyId key) {
+ return get_default_keychain().storage_get_contact_optimistic(storage_id, key);
+}
+Result<std::int64_t> storage_blockchain_height(StorageId storage_id) {
+ return get_default_keychain().storage_blockchain_height(storage_id);
+}
+Result<StorageUpdates> storage_blockchain_apply_block(StorageId storage_id, Slice block) {
+ return get_default_keychain().storage_blockchain_apply_block(storage_id, to_slice(block));
+}
+Result<Ok> storage_blockchain_add_proof(StorageId storage_id, Slice proof, const std::vector<std::string> &keys) {
+ return get_default_keychain().storage_blockchain_add_proof(storage_id, to_slice(proof), keys);
+}
+Result<StorageBlockchainState> storage_get_blockchain_state(StorageId storage_id) {
+ return get_default_keychain().storage_get_blockchain_state(storage_id);
+}
+
+Result<Bytes> call_create_zero_block(PrivateKeyId private_key_id, const CallState &initial_state) {
+ return get_default_keychain().call_create_zero_block(private_key_id, initial_state);
+}
+Result<Bytes> call_create_self_add_block(PrivateKeyId private_key_id, Slice previous_block,
+ const CallParticipant &self) {
+ return get_default_keychain().call_create_self_add_block(private_key_id, to_slice(previous_block), self);
+}
+Result<CallId> call_create(UserId user_id, PrivateKeyId private_key_id, Slice last_block) {
+ return get_default_keychain().call_create(user_id, private_key_id, to_slice(last_block));
+}
+Result<std::string> call_describe(CallId call_id) {
+ return get_default_keychain().call_describe(call_id);
+}
+Result<std::string> call_describe_block(Slice block_slice) {
+ bool is_server = tde2e_core::Blockchain::is_from_server(to_slice(block_slice));
+ TRY_RESULT(block_str, tde2e_core::Blockchain::from_any_to_local(std::string(block_slice)));
+ td::TlParser parser(block_str);
+ auto magic = parser.fetch_int();
+ if (magic != td::e2e_api::e2e_chain_block::ID) {
+ return td::Status::Error("Wrong magic");
+ }
+ auto block = td::e2e_api::e2e_chain_block::fetch(parser);
+ parser.fetch_end();
+ TRY_STATUS(parser.get_status());
+ return PSTRING() << (is_server ? "Server:" : "Local:") << to_string(block);
+}
+Result<std::string> call_describe_message(Slice broadcast_slice) {
+ bool is_server = tde2e_core::Blockchain::is_from_server(to_slice(broadcast_slice));
+ TRY_RESULT(broadcast_str, tde2e_core::Blockchain::from_any_to_local(std::string(broadcast_slice)));
+
+ td::TlParser parser(broadcast_str);
+ auto broadcast = td::e2e_api::e2e_chain_GroupBroadcast::fetch(parser);
+ parser.fetch_end();
+ TRY_STATUS(parser.get_status());
+ return PSTRING() << (is_server ? "Server:" : "Local:") << to_string(broadcast);
+}
+Result<Bytes> call_create_change_state_block(CallId call_id, const CallState &new_state) {
+ return get_default_keychain().call_create_change_state_block(call_id, new_state);
+}
+Result<SecureBytes> call_export_shared_key(CallId call_id) {
+ return get_default_keychain().call_export_shared_key(call_id);
+}
+Result<Bytes> call_encrypt(CallId call_id, CallChannelId channel_id, SecureSlice message) {
+ return get_default_keychain().call_encrypt(call_id, channel_id, to_slice(message));
+}
+Result<SecureBytes> call_decrypt(CallId call_id, UserId user_id, CallChannelId channel_id, Slice message) {
+ return get_default_keychain().call_decrypt(call_id, user_id, channel_id, to_slice(message));
+}
+Result<int> call_get_height(CallId call_id) {
+ return get_default_keychain().call_get_height(call_id);
+}
+Result<CallState> call_apply_block(CallId call_id, Slice block) {
+ return get_default_keychain().call_apply_block(call_id, to_slice(block));
+}
+Result<CallState> call_get_state(CallId call_id) {
+ return get_default_keychain().call_get_state(call_id);
+}
+Result<CallVerificationState> call_get_verification_state(CallId call_id) {
+ return get_default_keychain().call_get_verification_state(call_id);
+}
+Result<CallVerificationState> call_receive_inbound_message(CallId call_id, Slice message) {
+ return get_default_keychain().call_receive_inbound_message(call_id, to_slice(message));
+}
+Result<std::vector<Bytes>> call_pull_outbound_messages(CallId call_id) {
+ return get_default_keychain().call_pull_outbound_messages(call_id);
+}
+
+Result<CallVerificationWords> call_get_verification_words(CallId call_id) {
+ return get_default_keychain().call_get_verification_words(call_id);
+}
+Result<Ok> call_destroy(CallId call_id) {
+ return get_default_keychain().call_destroy(call_id);
+}
+Result<Ok> call_destroy_all() {
+ return get_default_keychain().call_destroy({});
+}
+
+// instantiations of templates
+template Result<UpdateId> storage_update_contact<UserId>(StorageId storage_id, PublicKeyId key,
+ SignedEntry<UserId> signed_entry);
+
+template Result<SignedEntry<UserId>> storage_sign_entry<UserId>(PrivateKeyId key, Entry<UserId> entry);
+
+template Result<UpdateId> storage_update_contact<Name>(StorageId storage_id, PublicKeyId key,
+ SignedEntry<Name> signed_entry);
+
+template Result<SignedEntry<Name>> storage_sign_entry<Name>(PrivateKeyId key, Entry<Name> entry);
+
+template Result<UpdateId> storage_update_contact<PhoneNumber>(StorageId storage_id, PublicKeyId key,
+ SignedEntry<PhoneNumber> signed_entry);
+
+template Result<SignedEntry<PhoneNumber>> storage_sign_entry<PhoneNumber>(PrivateKeyId key, Entry<PhoneNumber> entry);
+
+template Result<UpdateId> storage_update_contact<EmojiNonces>(StorageId storage_id, PublicKeyId key,
+ SignedEntry<EmojiNonces> signed_entry);
+
+template Result<SignedEntry<EmojiNonces>> storage_sign_entry<EmojiNonces>(PrivateKeyId key, Entry<EmojiNonces> entry);
+
+template Result<UpdateId> storage_update_contact<ContactState>(StorageId storage_id, PublicKeyId key,
+ SignedEntry<ContactState> signed_entry);
+
+template Result<SignedEntry<ContactState>> storage_sign_entry<ContactState>(PrivateKeyId key,
+ Entry<ContactState> entry);
+
+} // namespace tde2e_api
diff --git a/tde2e/td/e2e/e2e_api.h b/tde2e/td/e2e/e2e_api.h
new file mode 100644
index 000000000..1c25c2228
--- /dev/null
+++ b/tde2e/td/e2e/e2e_api.h
@@ -0,0 +1,336 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include "td/e2e/e2e_errors.h"
+
+#include <array>
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <tuple>
+#include <utility>
+#include <variant>
+#include <vector>
+
+namespace td {
+template <class T>
+class Result;
+class Status;
+} // namespace td
+
+namespace tde2e_api {
+//
+// Result and Error helper classes
+//
+
+struct Error {
+ ErrorCode code;
+ std::string message;
+};
+
+template <typename T>
+class Result {
+ public:
+ Result(const T &value) : data_(value) {
+ }
+ Result(T &&value) : data_(std::move(value)) {
+ }
+
+ Result(const Error &error) : data_(error) {
+ }
+ Result(Error &&error) : data_(std::move(error)) {
+ }
+
+ Result(td::Result<T> &&value);
+ Result(td::Status &&status);
+
+ // Check if the result is a success
+ bool is_ok() const {
+ return std::holds_alternative<T>(data_);
+ }
+
+ T &value() {
+ return std::get<T>(data_);
+ }
+ const T &value() const {
+ return std::get<T>(data_);
+ }
+
+ Error &error() {
+ return std::get<Error>(data_);
+ }
+ const Error &error() const {
+ return std::get<Error>(data_);
+ }
+
+ private:
+ std::variant<T, Error> data_;
+};
+
+//
+// Encryption
+//
+
+using Int256 = std::array<unsigned char, 32>;
+using Int512 = std::array<unsigned char, 64>;
+//TODO: strong typed ids
+using PublicKey = std::string;
+using HandshakeId = std::int64_t;
+using LoginId = std::int64_t;
+using UserId = std::int64_t;
+using AnyKeyId = std::int64_t;
+using PrivateKeyId = std::int64_t;
+using PublicKeyId = std::int64_t;
+using SymmetricKeyId = std::int64_t;
+using Bytes = std::string;
+using SecureBytes = std::string;
+using Slice = std::string_view;
+using SecureSlice = std::string_view;
+struct Ok {};
+
+struct EncryptedMessageForMany {
+ std::vector<std::string> encrypted_headers;
+ std::string encrypted_message;
+};
+
+Result<Ok> set_log_verbosity_level(int level);
+
+// Keys management
+// private keys will stay inside the library when it is possible
+// all keys are stored only in memory and should be created or imported before usage
+Result<PrivateKeyId> key_generate_private_key();
+Result<PrivateKeyId> key_generate_temporary_private_key();
+Result<SymmetricKeyId> key_derive_secret(PrivateKeyId key_id, Slice tag);
+Result<SymmetricKeyId> key_from_bytes(SecureSlice secret);
+Result<Bytes> key_to_encrypted_private_key(PrivateKeyId key_id, SymmetricKeyId secret_id);
+Result<PrivateKeyId> key_from_encrypted_private_key(Slice encrypted_key, SymmetricKeyId secret_id);
+Result<PublicKeyId> key_from_public_key(Slice public_key);
+Result<SymmetricKeyId> key_from_ecdh(PrivateKeyId key_id, PublicKeyId other_public_key_id);
+Result<PublicKey> key_to_public_key(PrivateKeyId key_id);
+Result<SecureBytes> key_to_words(PrivateKeyId key_id);
+Result<PrivateKeyId> key_from_words(SecureSlice words);
+Result<Int512> key_sign(PrivateKeyId key, Slice data);
+Result<Ok> key_destroy(AnyKeyId key_id);
+Result<Ok> key_destroy_all();
+
+// Used to encrypt key between processes, secret_id must be generated with key_from_ecdh
+Result<Bytes> key_to_encrypted_private_key_internal(PrivateKeyId key_id, SymmetricKeyId secret_id);
+Result<PrivateKeyId> key_from_encrypted_private_key_internal(Slice encrypted_key, SymmetricKeyId secret_id);
+
+Result<EncryptedMessageForMany> encrypt_message_for_many(const std::vector<SymmetricKeyId> &key_ids,
+ SecureSlice message);
+// keeps encrypted_message empty in result
+Result<EncryptedMessageForMany> re_encrypt_message_for_many(SymmetricKeyId decrypt_key_id,
+ const std::vector<SymmetricKeyId> &encrypt_key_ids,
+ Slice encrypted_header, Slice encrypted_message);
+Result<SecureBytes> decrypt_message_for_many(SymmetricKeyId key_id, Slice encrypted_header, Slice encrypted_message);
+Result<Bytes> encrypt_message_for_one(SymmetricKeyId key_id, SecureSlice message);
+Result<SecureBytes> decrypt_message_for_one(SymmetricKeyId key_id, Slice encrypted_message);
+
+// Utilities for secret key verification/transfer via qr (or any other alternative channel)
+// Requires:
+// - alternative channel to reliably transfer 'start' message from Bob to Alice (scanning QR is such channel)
+//
+// Alice:
+// - knows shared secret right after the handshake creation
+// - Bob's secret key is verified after finish is received
+//
+// Bob:
+// - knows shared secret right after accept is received
+// - Alice's secret key is verified after accept is received
+//
+// Use cases:
+// - Transfer of the key from old device to the new device
+// - Verification of other person's public key
+// - Contact sharing
+//
+Result<HandshakeId> handshake_create_for_bob(UserId bob_user_id, PrivateKeyId bob_private_key_id);
+Result<HandshakeId> handshake_create_for_alice(UserId alice_user_id, PrivateKeyId alice_private_key_id,
+ UserId bob_user_id, const PublicKey &bob_public_key, Slice start);
+
+Result<Bytes> handshake_bob_send_start(HandshakeId bob_handshake_id);
+Result<Bytes> handshake_alice_send_accept(HandshakeId alice_handshake_id);
+Result<Bytes> handshake_bob_receive_accept_send_finish(HandshakeId bob_handshake_id, UserId alice_id,
+ const PublicKey &alice_public_key, Slice accept);
+Result<Ok> handshake_alice_receive_finish(HandshakeId alice_handshake_id, Slice finish);
+Result<SymmetricKeyId> handshake_get_shared_key_id(HandshakeId handshake_id);
+Result<Ok> handshake_destroy(HandshakeId handshake_id);
+Result<Ok> handshake_destroy_all();
+
+// Helper to get QR-code identifier
+Result<Bytes> handshake_start_id(Slice start);
+
+// There is wrapper for login
+Result<LoginId> login_create_for_bob();
+Result<Bytes> login_bob_send_start(LoginId bob_login_id);
+Result<Bytes> login_create_for_alice(UserId alice_user_id, PrivateKeyId alice_private_key_id, Slice start);
+Result<PrivateKeyId> login_finish_for_bob(LoginId bob_login_id, UserId alice_user_id, const PublicKey &alice_public_key,
+ Slice data);
+Result<Ok> login_destroy(LoginId login_id);
+Result<Ok> login_destroy_all();
+
+// Personal info
+
+// 1. Each entry stored and signed separately
+// 2. Signature is never stored, but always is verified
+// 3. It should be possible to save data without signature (but it can't override data with signature)
+// 4. We should keep source of entry. Our, Server, Contact+Ts
+// 5. We must keep is_contact flag.
+
+template <class T>
+struct Entry {
+ enum Source { Self, Server, Contact };
+ Source source;
+ std::uint32_t timestamp;
+ T value;
+
+ Entry() : source(Self), timestamp(0), value() {
+ }
+ Entry(Source source, std::uint32_t timestamp, T &&value)
+ : source(source), timestamp(timestamp), value(std::move(value)) {
+ }
+};
+
+template <class T>
+struct SignedEntry {
+ Int512 signature;
+ std::uint32_t timestamp{0};
+ T value;
+};
+
+struct Name {
+ std::string first_name;
+ std::string last_name;
+};
+
+struct PhoneNumber {
+ std::string phone_number;
+};
+
+struct EmojiNonces {
+ std::optional<Int256> self_nonce;
+ std::optional<Int256> contact_nonce_hash;
+ std::optional<Int256> contact_nonce;
+};
+
+struct ContactState {
+ enum State { Unknown, Contact, NotContact };
+ State state{Unknown};
+
+ ContactState() = default;
+
+ explicit ContactState(State state) : state(state) {
+ }
+};
+
+struct Contact {
+ // Each contact has both public_key and user_id
+ // how it is stored internally is not clients library user's concern
+ std::uint32_t generation{0};
+
+ // we always have public key. Essentially contact is defined by its public key
+ PublicKeyId public_key{};
+
+ // Personal data, signed by contact. If it is not signed, it has no relations to this public key
+ std::optional<Entry<UserId>> o_user_id;
+ std::optional<Entry<Name>> o_name;
+ std::optional<Entry<PhoneNumber>> o_phone_number;
+
+ // Personal data save by user_itself
+ // It always exists and it always given but we keep timestamp just in case
+ Entry<EmojiNonces> emoji_nonces;
+ Entry<ContactState> contact_state;
+};
+
+// library can't enforce persistency of changes, because it has no persistent state.
+// so it is client's responsibility to ensure that each change will be eventually saved to the server
+// NB: it is unclear how protect ourself from server ignoring our queries.
+
+using StorageId = std::int64_t;
+using UpdateId = std::int64_t;
+
+struct StorageBlockchainState {
+ std::string next_suggested_block;
+ std::vector<std::string> required_proofs;
+};
+struct StorageUpdates {
+ std::vector<std::pair<PublicKeyId, std::optional<Contact>>> updates;
+};
+
+Result<StorageId> storage_create(PrivateKeyId key_id, Slice last_block);
+Result<Ok> storage_destroy(StorageId storage_id);
+Result<Ok> storage_destroy_all();
+
+template <class T>
+Result<UpdateId> storage_update_contact(StorageId storage_id, PublicKeyId key, SignedEntry<T> signed_entry);
+template <class T>
+Result<SignedEntry<T>> storage_sign_entry(PrivateKeyId key, Entry<T> entry);
+
+Result<std::optional<Contact>> storage_get_contact(StorageId storage_id, PublicKeyId key);
+Result<std::optional<Contact>> storage_get_contact_optimistic(StorageId storage_id, PublicKeyId key);
+
+Result<std::int64_t> storage_blockchain_height(StorageId storage_id);
+Result<StorageUpdates> storage_blockchain_apply_block(StorageId storage_id, Slice block);
+Result<Ok> storage_blockchain_add_proof(StorageId storage_id, Slice proof, const std::vector<std::string> &keys);
+
+Result<StorageBlockchainState> storage_get_blockchain_state(StorageId);
+
+using CallId = std::int64_t;
+using CallChannelId = std::int32_t;
+struct CallParticipant {
+ UserId user_id;
+ PublicKeyId public_key_id;
+ int permissions{};
+};
+
+struct CallState {
+ int height{};
+ std::vector<CallParticipant> participants;
+};
+
+Result<Bytes> call_create_zero_block(PrivateKeyId private_key_id, const CallState &initial_state);
+Result<Bytes> call_create_self_add_block(PrivateKeyId private_key_id, Slice previous_block,
+ const CallParticipant &self);
+Result<CallId> call_create(UserId user_id, PrivateKeyId private_key_id, Slice last_block);
+
+Result<std::string> call_describe(CallId call);
+Result<std::string> call_describe_block(Slice block);
+Result<std::string> call_describe_message(Slice message);
+
+Result<Bytes> call_create_change_state_block(CallId call_id, const CallState &new_state);
+Result<Bytes> call_encrypt(CallId call_id, CallChannelId channel_id, SecureSlice message);
+Result<SecureBytes> call_decrypt(CallId call_id, UserId user_id, CallChannelId channel_id, Slice message);
+
+Result<int> call_get_height(CallId call_id);
+Result<CallState> call_apply_block(CallId call_id, Slice block);
+
+Result<CallState> call_get_state(CallId call_id);
+
+struct CallVerificationState {
+ int height{};
+ std::optional<Bytes> emoji_hash;
+};
+Result<CallVerificationState> call_get_verification_state(CallId call_id);
+Result<CallVerificationState> call_receive_inbound_message(CallId call_id, Slice message);
+
+// should be called after:
+// - creation
+// - call_apply_block
+// - call_receive_inbound_messages
+Result<std::vector<Bytes>> call_pull_outbound_messages(CallId call_id);
+
+struct CallVerificationWords {
+ int height{};
+ std::vector<std::string> words;
+};
+
+Result<CallVerificationWords> call_get_verification_words(CallId call_id);
+Result<Ok> call_destroy(CallId call_id);
+Result<Ok> call_destroy_all();
+
+} // namespace tde2e_api
diff --git a/tde2e/td/e2e/e2e_errors.h b/tde2e/td/e2e/e2e_errors.h
new file mode 100644
index 000000000..686311418
--- /dev/null
+++ b/tde2e/td/e2e/e2e_errors.h
@@ -0,0 +1,102 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include <string_view>
+
+namespace tde2e_api {
+
+enum class ErrorCode : int {
+ UnknownError = 100,
+ Any = 101,
+ InvalidInput = 102,
+ InvalidKeyId = 103,
+ InvalidId = 104,
+ InvalidBlock = 200,
+ InvalidBlock_NoChanges = 201,
+ InvalidBlock_InvalidSignature = 202,
+ InvalidBlock_HashMismatch = 203,
+ InvalidBlock_HeightMismatch = 204,
+ InvalidBlock_InvalidStateProof_Group = 205,
+ InvalidBlock_InvalidStateProof_Secret = 206,
+ InvalidBlock_NoPermissions = 207,
+ InvalidBlock_InvalidGroupState = 208,
+ InvalidCallGroupState_NotParticipant = 300,
+ InvalidCallGroupState_WrongUserId = 301,
+ Decrypt_UnknownEpoch = 400,
+ Encrypt_UnknownEpoch = 401,
+ InvalidBroadcast_InFuture = 500,
+ InvalidBroadcast_NotInCommit = 501,
+ InvalidBroadcast_NotInReveal = 502,
+ InvalidBroadcast_UnknownUserId = 503,
+ InvalidBroadcast_AlreadyApplied = 504,
+ InvalidBroadcast_InvalidReveal = 505,
+ InvalidBroadcast_InvalidBlockHash = 506,
+ InvalidCallChannelId = 600,
+ CallFailed = 601
+};
+inline std::string_view error_string(ErrorCode error_code) {
+ switch (error_code) {
+ case ErrorCode::Any:
+ return "";
+ case ErrorCode::UnknownError:
+ return "UNKNOWN_ERROR";
+ case ErrorCode::InvalidInput:
+ return "INVALID_INPUT";
+ case ErrorCode::InvalidKeyId:
+ return "INVALID_KEY_ID";
+ case ErrorCode::InvalidId:
+ return "INVALID_ID";
+ case ErrorCode::InvalidBlock:
+ return "INVALID_BLOCK";
+ case ErrorCode::InvalidBlock_NoChanges:
+ return "INVALID_BLOCK__NO_CHANGES";
+ case ErrorCode::InvalidBlock_InvalidSignature:
+ return "INVALID_BLOCK__INVALID_SIGNATURE";
+ case ErrorCode::InvalidBlock_HashMismatch:
+ return "INVALID_BLOCK__HASH_MISMATCH";
+ case ErrorCode::InvalidBlock_HeightMismatch:
+ return "INVALID_BLOCK__HEIGHT_MISMATCH";
+ case ErrorCode::InvalidBlock_InvalidStateProof_Group:
+ return "INVALID_BLOCK__INVALID_STATE_PROOF__GROUP";
+ case ErrorCode::InvalidBlock_InvalidStateProof_Secret:
+ return "INVALID_BLOCK__INVALID_STATE_PROOF__SECRET";
+ case ErrorCode::InvalidBlock_InvalidGroupState:
+ return "INVALID_BLOCK__INVALID_GROUP_STATE";
+ case ErrorCode::InvalidBlock_NoPermissions:
+ return "INVALID_BLOCK__NO_PERMISSIONS";
+ case ErrorCode::InvalidCallGroupState_NotParticipant:
+ return "INVALID_CALL_GROUP_STATE__NOT_PARTICIPANT";
+ case ErrorCode::InvalidCallGroupState_WrongUserId:
+ return "INVALID_CALL_GROUP_STATE__WRONG_USER_ID";
+ case ErrorCode::Decrypt_UnknownEpoch:
+ return "DECRYPT__UNKNOWN_EPOCH";
+ case ErrorCode::Encrypt_UnknownEpoch:
+ return "ENCRYPT__UNKNOWN_EPOCH";
+ case ErrorCode::InvalidBroadcast_InFuture:
+ return "INVALID_BROADCAST__IN_FUTURE";
+ case ErrorCode::InvalidBroadcast_NotInCommit:
+ return "INVALID_BROADCAST__NOT_IN_COMMIT";
+ case ErrorCode::InvalidBroadcast_NotInReveal:
+ return "INVALID_BROADCAST__NOT_IN_REVEAL";
+ case ErrorCode::InvalidBroadcast_UnknownUserId:
+ return "INVALID_BROADCAST__UNKNOWN_USER_ID";
+ case ErrorCode::InvalidBroadcast_AlreadyApplied:
+ return "INVALID_BROADCAST__ALREADY_APPLIED";
+ case ErrorCode::InvalidBroadcast_InvalidReveal:
+ return "INVALID_BROADCAST__INVALID_REVEAL";
+ case ErrorCode::InvalidBroadcast_InvalidBlockHash:
+ return "INVALID_BROADCAST__INVALID_BLOCK_HASH";
+ case ErrorCode::CallFailed:
+ return "CALL_FAILED";
+ case ErrorCode::InvalidCallChannelId:
+ return "INVALID_CALL_CHANNEL_ID";
+ }
+ return "UNKNOWN_ERROR";
+}
+
+} // namespace tde2e_api
diff --git a/tde2e/td/e2e/encryption_test.py b/tde2e/td/e2e/encryption_test.py
new file mode 100644
index 000000000..8df391cdc
--- /dev/null
+++ b/tde2e/td/e2e/encryption_test.py
@@ -0,0 +1,314 @@
+#
+# Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+#
+# Distributed under the Boost Software License, Version 1.0. (See accompanying
+# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+#
+import os
+from Crypto.Cipher import AES
+from Crypto.Hash import SHA256, HMAC, SHA512, SHA256
+import binascii
+import random
+import struct
+
+def generate_deterministic_padding(data_size, min_padding):
+ # Calculate padding size to make total size multiple of 16
+ padding_size = ((min_padding + 15 + data_size) & -16) - data_size
+ padding = bytearray(padding_size)
+
+ # Only set the first byte to padding size, leave rest as zeros
+ padding[0] = padding_size
+
+ return padding
+
+
+def hmac_sha512(a, b):
+ # Combine two secrets using HMAC-SHA512
+ if isinstance(a, str):
+ a = a.encode('utf-8')
+ if isinstance(b, str):
+ b = b.encode('utf-8')
+ hmac = HMAC.new(a, b, SHA512)
+ return hmac.digest()
+
+def hmac_sha256(a, b):
+ # Combine two secrets using HMAC-SHA512
+ if isinstance(a, str):
+ a = a.encode('utf-8')
+ if isinstance(b, str):
+ b = b.encode('utf-8')
+ hmac = HMAC.new(a, b, SHA256)
+ return hmac.digest()
+
+def kdf(key, info):
+ return hmac_sha512(key, info)
+
+def encode_len(extra):
+ return struct.pack('<i', len(extra))
+
+def encrypt_data_with_prefix(data, secret, extra=b""):
+ # Ensure data is multiple of 16 bytes
+ assert len(data) % 16 == 0
+
+ # Generate encryption and HMAC secrets
+ large_secret = kdf(secret, "tde2e_encrypt_data")
+ encrypt_secret = large_secret[:32]
+ hmac_secret = large_secret[32:64]
+
+ # Generate message ID using HMAC
+ large_msg_id = hmac_sha256(hmac_secret, data + extra + encode_len(extra))
+ msg_id = large_msg_id[:16] # Use first 16 bytes as message ID
+
+ # Create result buffer
+ result = bytearray(len(data) + 16)
+ result[0:16] = msg_id
+
+ # Generate key and IV for encryption
+ encryption_secret = hmac_sha512(encrypt_secret, msg_id)
+ key = encryption_secret[:32]
+ iv = encryption_secret[32:48]
+
+ # Encrypt data
+ cipher = AES.new(key, AES.MODE_CBC, iv)
+ encrypted = cipher.encrypt(data)
+ result[16:] = encrypted
+
+ return bytes(result)
+
+def encrypt_data_with_deterministic_padding(data, secret, extra):
+ # Generate deterministic padding
+ padding = generate_deterministic_padding(len(data), 16)
+
+ # Combine padding and data
+ combined = bytearray(len(padding) + len(data))
+ combined[0:len(padding)] = padding
+ combined[len(padding):] = data
+
+ # Encrypt the combined data
+ return encrypt_data_with_prefix(combined, secret, extra)
+
+def encrypt_header(header, encrypted_message, secret):
+ # Verify inputs
+ assert len(header) == 32
+ assert len(encrypted_message) >= 16
+
+ # Get msg_id from the beginning of encrypted message
+ msg_id = encrypted_message[0:16]
+
+ encryption_key = kdf(secret, "tde2e_encrypt_header")[:32]
+
+ # Generate encryption key and IV from secret and message ID
+ encryption_secret = kdf(encryption_key, msg_id)
+ key = encryption_secret[:32]
+ iv = encryption_secret[32:48]
+
+ # Encrypt header with AES-CBC
+ cipher = AES.new(key, AES.MODE_CBC, iv)
+ encrypted_header = cipher.encrypt(header)
+
+ return encrypted_header
+
+def decrypt_data(encrypted_data, secret, extra=b""):
+ # Verify input size
+ if len(encrypted_data) < 17:
+ raise ValueError("Failed to decrypt: data is too small")
+ if len(encrypted_data) % 16 != 0:
+ raise ValueError("Failed to decrypt: data size is not divisible by 16")
+
+ # Extract msg_id and encrypted part
+ msg_id = encrypted_data[0:16]
+ encrypted_part = encrypted_data[16:]
+
+ # Generate encryption and HMAC secrets
+ large_secret = kdf(secret, "tde2e_encrypt_data")
+ hmac_secret = large_secret[32:64]
+ encrypt_secret = large_secret[:32]
+
+ # Generate key and IV for decryption
+ encryption_secret = hmac_sha512(encrypt_secret, msg_id)
+ key = encryption_secret[:32]
+ iv = encryption_secret[32:48]
+
+ # Decrypt with AES-CBC
+ cipher = AES.new(key, AES.MODE_CBC, iv)
+ decrypted_data = cipher.decrypt(encrypted_part)
+
+ # Verify msg_id
+ large_msg_id = hmac_sha256(hmac_secret, decrypted_data + extra + encode_len(extra))
+ expected_msg_id = large_msg_id[:16]
+ if msg_id != expected_msg_id:
+ raise ValueError("Failed to decrypt: msg_id mismatch")
+
+ # Extract actual data by removing padding
+ prefix_size = decrypted_data[0]
+ if prefix_size > len(decrypted_data) or prefix_size < 16:
+ raise ValueError("Failed to decrypt: invalid prefix size")
+
+ return decrypted_data[prefix_size:]
+
+def decrypt_header(encrypted_header, encrypted_message, secret):
+ # Verify inputs
+ if len(encrypted_header) != 32:
+ raise ValueError("Failed to decrypt: invalid header size")
+ if len(encrypted_message) < 16:
+ raise ValueError("Failed to decrypt: invalid message size")
+
+ # Get msg_id from the beginning of encrypted message
+ msg_id = encrypted_message[0:16]
+ encryption_key = kdf(secret, "tde2e_encrypt_header")[:32]
+
+ # Generate encryption key and IV from secret and msg_id
+ encryption_secret = kdf(encryption_key, msg_id)
+ key = encryption_secret[:32]
+ iv = encryption_secret[32:48]
+
+ # Decrypt header with AES-CBC
+ cipher = AES.new(key, AES.MODE_CBC, iv)
+ decrypted_header = cipher.decrypt(encrypted_header)
+
+ return decrypted_header
+
+def generate_random_bytes(length):
+ return bytes(random.getrandbits(8) for _ in range(length))
+
+def generate_test_vectors():
+ # Generate random secrets and headers for each test
+ secret = generate_random_bytes(32)
+ header = generate_random_bytes(32)
+
+ # Test vectors with different data patterns
+ test_vectors = [
+ {
+ "name": "empty_message",
+ "secret": binascii.hexlify(secret).decode('ascii'),
+ "data": "",
+ "extra": "",
+ "header": binascii.hexlify(header).decode('ascii')
+ },
+ {
+ "name": "simple_message",
+ "secret": binascii.hexlify(secret).decode('ascii'),
+ "data": binascii.hexlify(b"Hello, World!").decode('ascii'),
+ "extra": "",
+ "header": binascii.hexlify(header).decode('ascii')
+ },
+ {
+ "name": "long_message",
+ "secret": binascii.hexlify(secret).decode('ascii'),
+ "data": binascii.hexlify(b"x" * 200).decode('ascii'),
+ "extra": "",
+ "header": binascii.hexlify(header).decode('ascii')
+ },
+ {
+ "name": "random_message",
+ "secret": binascii.hexlify(secret).decode('ascii'),
+ "data": binascii.hexlify(generate_random_bytes(64)).decode('ascii'),
+ "extra": binascii.hexlify(b"small extra").decode('ascii'),
+ "header": binascii.hexlify(header).decode('ascii')
+ },
+ {
+ "name": "very_long_message",
+ "secret": binascii.hexlify(secret).decode('ascii'),
+ "data": binascii.hexlify(generate_random_bytes(300)).decode('ascii'),
+ "extra": binascii.hexlify(generate_random_bytes(300)).decode('ascii'),
+ "header": binascii.hexlify(header).decode('ascii')
+ },
+ {
+ "name": "message_with_special_chars",
+ "secret": binascii.hexlify(secret).decode('ascii'),
+ "data": binascii.hexlify(bytes([i for i in range(33, 64)])).decode('ascii'),
+ "extra": "",
+ "header": binascii.hexlify(header).decode('ascii')
+ },
+ {
+ "name": "message_with_unicode",
+ "secret": binascii.hexlify(secret).decode('ascii'),
+ "data": binascii.hexlify("Hello, 世界!".encode('utf-8')).decode('ascii'),
+ "extra": "",
+ "header": binascii.hexlify(header).decode('ascii')
+ },
+ ]
+
+ # Generate encrypted data and headers for each test vector
+ for vec in test_vectors:
+ secret = binascii.unhexlify(vec["secret"])
+ data = binascii.unhexlify(vec["data"])
+ extra = binascii.unhexlify(vec["extra"])
+ header = binascii.unhexlify(vec["header"])
+
+ encrypted = encrypt_data_with_deterministic_padding(data, secret, extra)
+ encrypted_header = encrypt_header(header, encrypted, secret)
+
+ # Test decryption
+ decrypted = decrypt_data(encrypted, secret, extra)
+ if decrypted != data:
+ raise ValueError(f"Decryption failed for test vector: {vec['name']}")
+
+ # Test header decryption
+ decrypted_header = decrypt_header(encrypted_header, encrypted, secret)
+ if decrypted_header != header:
+ raise ValueError(f"Header decryption failed for test vector: {vec['name']}")
+
+ vec["encrypted"] = binascii.hexlify(encrypted).decode('ascii')
+ vec["encrypted_header"] = binascii.hexlify(encrypted_header).decode('ascii')
+
+ return test_vectors
+
+def print_cpp_header(test_vectors):
+ print("//")
+ print("// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025")
+ print("//")
+ print("// Distributed under the Boost Software License, Version 1.0. (See accompanying")
+ print("// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)")
+ print("//")
+ print("#pragma once")
+ print("\n#include <string>")
+ print("#include <vector>")
+ print("\nnamespace tde2e_core {")
+ print("\nstruct TestVector {")
+ print(" std::string name;")
+ print(" std::string secret;")
+ print(" std::string data;")
+ print(" std::string extra;")
+ print(" std::string header;")
+ print(" std::string encrypted;")
+ print(" std::string encrypted_header;")
+ print("};")
+ print("\ninline std::vector<TestVector> get_test_vectors() {")
+ print(" return {")
+
+ for vec in test_vectors:
+ print(" {")
+ print(f' "{vec["name"]}",')
+ print(f' "{vec["secret"]}",')
+ print(f' "{vec["data"]}",')
+ print(f' "{vec["extra"]}",')
+ print(f' "{vec["header"]}",')
+ print(f' "{vec["encrypted"]}",')
+ print(f' "{vec["encrypted_header"]}"')
+ print(" },")
+
+ print(" };")
+ print("}")
+ print("\n} // namespace tde2e_core")
+
+if __name__ == "__main__":
+ test_vectors = generate_test_vectors()
+
+ # Get the directory of the current script
+ script_dir = os.path.dirname(os.path.abspath(__file__))
+ # Go up two levels to reach the tde2e directory
+ tde2e_dir = os.path.dirname(os.path.dirname(script_dir))
+ # Create the test directory if it doesn't exist
+ test_dir = os.path.join(tde2e_dir, "test")
+ os.makedirs(test_dir, exist_ok=True)
+
+ # Write the header file
+ header_path = os.path.join(test_dir, "EncryptionTestVectors.h")
+ with open(header_path, "w") as f:
+ # Redirect print output to the file
+ import sys
+ old_stdout = sys.stdout
+ sys.stdout = f
+ print_cpp_header(test_vectors)
+ sys.stdout = old_stdout
diff --git a/tde2e/td/e2e/utils.h b/tde2e/td/e2e/utils.h
new file mode 100644
index 000000000..628697340
--- /dev/null
+++ b/tde2e/td/e2e/utils.h
@@ -0,0 +1,151 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include "td/e2e/e2e_api.h"
+#include "td/e2e/e2e_errors.h"
+#include "td/e2e/Keys.h"
+
+#include "td/utils/as.h"
+#include "td/utils/int_types.h"
+#include "td/utils/Random.h"
+#include "td/utils/SharedSlice.h"
+#include "td/utils/Slice.h"
+#include "td/utils/SliceBuilder.h"
+#include "td/utils/Status.h"
+#include "td/utils/tl_helpers.h"
+#include "td/utils/tl_storers.h"
+#include "td/utils/UInt.h"
+
+#include <string>
+#include <utility>
+
+namespace tde2e_api {
+
+inline Error to_error(const td::Status &status) {
+ auto error_code = ErrorCode(status.code());
+ if (error_string(error_code) == "UNKNOWN_ERROR") {
+ return Error{ErrorCode::UnknownError, status.message().str()};
+ }
+ return Error{error_code, status.message().str()};
+}
+
+template <class T>
+Result<T> to_result(td::Result<T> &value) {
+ if (value.is_ok()) {
+ return Result<T>(value.move_as_ok());
+ }
+ return Result<T>(to_error(value.error()));
+}
+
+template <typename T>
+Result<T>::Result(td::Result<T> &&value) : Result(to_result(value)) {
+}
+
+template <typename T>
+Result<T>::Result(td::Status &&status) : Result(to_error(status)) {
+}
+
+} // namespace tde2e_api
+
+namespace tde2e_core {
+
+using E = tde2e_api::ErrorCode;
+
+inline td::Status Error(E error_code) {
+ auto msg = tde2e_api::error_string(error_code);
+ return td::Status::Error(static_cast<int>(error_code), td::Slice(msg.data(), msg.size()));
+}
+
+inline td::Status Error(E error_code, td::Slice message) {
+ auto msg = tde2e_api::error_string(error_code);
+ return td::Status::Error(static_cast<int>(error_code), PSLICE()
+ << td::Slice(msg.data(), msg.size()) << ": " << message);
+}
+
+template <typename T, typename = void>
+constexpr bool has_static_ID = false;
+
+template <typename T>
+constexpr bool has_static_ID<T, decltype((void)T::ID, void())> = true;
+
+template <class T>
+std::string serialize_boxed(const T &object) {
+ if constexpr (has_static_ID<T>) {
+ auto suffix = serialize(object);
+ std::string result(4 + suffix.size(), 0);
+ td::TlStorerUnsafe storer(td::MutableSlice(result).ubegin());
+ storer.store_int(T::ID);
+ storer.store_slice(suffix);
+ return result;
+ } else {
+ return td::serialize(object);
+ }
+}
+
+template <class T>
+td::SecureString serialize_boxed_secure(const T &object) {
+ if constexpr (has_static_ID<T>) {
+ auto suffix = td::serialize_secure(object);
+ td::SecureString result(4 + suffix.size(), 0);
+ td::TlStorerUnsafe storer(result.as_mutable_slice().ubegin());
+ storer.store_int(T::ID);
+ storer.store_slice(suffix);
+ return result;
+ } else {
+ return td::serialize_secure(object);
+ }
+}
+
+struct UInt256Hash {
+ td::uint32 operator()(const td::UInt256 v) const {
+ return td::as<td::uint32>(v.raw);
+ }
+};
+
+inline td::UInt256 generate_nonce() {
+ td::UInt256 nonce;
+ td::Random::secure_bytes(nonce.as_mutable_slice());
+ return nonce;
+}
+
+template <class T>
+td::Status verify_signature(const PublicKey &public_key, T &signed_tl_object) {
+ auto signature = signed_tl_object.signature_;
+ signed_tl_object.signature_ = {};
+ auto to_sign = serialize_boxed(signed_tl_object);
+ auto result = public_key.verify(to_sign, Signature::from_u512(signature));
+ signed_tl_object.signature_ = signature;
+ if (result.is_error()) {
+ return Error(E::InvalidBlock_InvalidSignature, result.message());
+ }
+ return result;
+}
+
+template <class T>
+td::Result<Signature> sign(const PrivateKey &private_key, T &unsigned_tl_object) {
+ unsigned_tl_object.signature_ = {};
+ auto to_sign = serialize_boxed(unsigned_tl_object);
+ return private_key.sign(to_sign);
+}
+
+template <class T>
+td::Result<T> to_td(tde2e_api::Result<T> &&r) {
+ if (r.is_ok()) {
+ return std::move(r.value());
+ }
+ return td::Status::Error(static_cast<int>(r.error().code), r.error().message);
+}
+
+inline td::Status to_td(tde2e_api::Result<tde2e_api::Ok> &r) {
+ if (r.is_ok()) {
+ return td::Status::OK();
+ }
+ return td::Status::Error(static_cast<int>(r.error().code), r.error().message);
+}
+
+} // namespace tde2e_core
diff --git a/tde2e/test/EncryptionTestVectors.h b/tde2e/test/EncryptionTestVectors.h
new file mode 100644
index 000000000..98f23a198
--- /dev/null
+++ b/tde2e/test/EncryptionTestVectors.h
@@ -0,0 +1,88 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#pragma once
+
+#include <string>
+#include <vector>
+
+namespace tde2e_core {
+
+struct TestVector {
+ std::string name;
+ std::string secret;
+ std::string data;
+ std::string extra;
+ std::string header;
+ std::string encrypted;
+ std::string encrypted_header;
+};
+
+inline std::vector<TestVector> get_test_vectors() {
+ return {
+ {"empty_message", "f9fb473b9887e50ea38eef7380c82361432cd4b22c5f9b3700809990d8ed344c", "", "",
+ "bd29703cf44551710ca14d091a6c98ee347931b2b8140faaaef2dbb40719df12",
+ "d28eb3e3d1328f06dafedabd67a353d5ea6e164d2f34c162a16f8a1164663a03",
+ "4060edd7bcacca6dd0f4fe81d6ec63a8859fa9d520598043bc4748919f3fdeda"},
+ {"simple_message", "f9fb473b9887e50ea38eef7380c82361432cd4b22c5f9b3700809990d8ed344c",
+ "48656c6c6f2c20576f726c6421", "", "bd29703cf44551710ca14d091a6c98ee347931b2b8140faaaef2dbb40719df12",
+ "967f5245b03e07ab7be6044174306a4af811e96708ae3ad2ab427aa5495508b1c319ca0353531c0a2921e307f2455856",
+ "9e7910949e526b6ad51a59aad8022c826b00f379e28592ed3216aabc6be252e0"},
+ {"long_message", "f9fb473b9887e50ea38eef7380c82361432cd4b22c5f9b3700809990d8ed344c",
+ "787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787"
+ "878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878"
+ "787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787"
+ "8787878787878787878787878787878787878787878787878787878787878787878",
+ "", "bd29703cf44551710ca14d091a6c98ee347931b2b8140faaaef2dbb40719df12",
+ "8202a46a19de7111166f6c244127c84dbdc1c3a63ca6526dc699c6cbbc6f8236ee82a0172ed1115cb4a2ba8e27cfa8089822c7e9070ec2f"
+ "5c7cee77cc979447c1db9758119ad28a05b2edfc1c98b486985bb60fb6f1fefb4b5f7ecea19d59b8018f865a7be3771c7f6fe6092b34b78"
+ "a1bddefc8d07f2f61351a4247c41a58cb068ebe9110245de4fda076f0ff73aede4e9811678424f648b8054b921b53f0612dfbdb7173a86b"
+ "ce3eba73955afef435599ae34825d295e8d298d6d3a1fc07084740c0b1c3a24cebedbd26b631cbbd1a352c1a499ba3576a628a74ab14eb1"
+ "d180e5af7e9eac4020b889fafc4f7bfb2e24",
+ "641620351a1e4d76711385d5cf3b0eed07308c9cafc06ef09ed0c1f57ebb5f42"},
+ {"random_message", "f9fb473b9887e50ea38eef7380c82361432cd4b22c5f9b3700809990d8ed344c",
+ "fed306c137017ab008c22d1f74bc104e5138d3c19b42fb303c768b083912a1102d06ac1e0f4440e3b32b9144a50e6fc0f190273bd4dec7e"
+ "f847bf7d46680bb67",
+ "736d616c6c206578747261", "bd29703cf44551710ca14d091a6c98ee347931b2b8140faaaef2dbb40719df12",
+ "ee198e4860c888ab18bcecba5083eb539f402be1fbde51e0c49e398145d40ebb7b5a52bdc83d6200e63a70c47cc6a5e6fa7a71f6e24b722"
+ "b5f0314b6b52768dbd2e438a582d1cf2a54d4de7ba30e36e68afd8379b63a345483dbcc33380fd07f",
+ "f282da8a41f17a5fa7f793c6c134c5bb2b960a2fea43bdc15b58a69cb7dafb43"},
+ {"very_long_message", "f9fb473b9887e50ea38eef7380c82361432cd4b22c5f9b3700809990d8ed344c",
+ "d16554889d83850ffb42d119e0c69d8b68ee07ff021f0a2cb7beb70d0b1cc62e3d8fe2dff95e674893393b5da015a965108c785d8935a3e"
+ "e58e3df9505016020b558687ee535f9bcfa94450ded18ac3e8145879af43e66eeacfee1d9f9c9c78824cf34639af50fb0b93de73aa9362c"
+ "f2732e2d8c652111ec1246c8ded3b19e93d154d04cc8a4bd927332136d7627e71e6be2c97dd62235dfd998d1e630588d10beeab791e0919"
+ "9bfa8bab3b9e6dbcdfba9f9dd76110f7f6c7fd1fbccc421e7ad093e8fd385e53e3c03f7f0a79296962de1e752eea5f8c5e6325ef406aef5"
+ "62d8ef0b9431defeb46fb93ee3c3409af0e3a4f7e63af4efbfb5f4b61a104c1158247877a28f9538d6cf8c5e243ece977cc2a0a0bcf602c"
+ "d16df445cb71a4f6a0494a3b6a1149725c169dc40eb10",
+ "ebc6b1176ca69b8bb769bcc68add44fbba1c79d2771ef412eccad3ee4f7afe595f8fd2052f8d1d8b8fea209c568eb6a4c6aea6d88c583df"
+ "25ed3f38260c2f95c1f0244219d55e658498b34f7e7a527c60723b6806fe28275337b0c9b64c158825a3c14d8cec6a40bbf8c5a5a8009ca"
+ "75f2c6f2e7f3ab612ff5d675f2c3b801d4d4e0408b49d8543d8621de0df26a65a49d1fc7a21584d5495a24b2090479870e852766f6de34b"
+ "724e5941097d19153f4f4d035ae0c978ec6354ba452cf465581cd4afd7045bfa4c54383796587d19e981da220cd9ca5230161eaf64d8a1b"
+ "406a2f8afc7faeb0ec7634c3c14aa63736c955b56c48c61ba58b109775ac252f3837e8bcebdb40f4ca2ce32609619b0063cb421a268f80c"
+ "60ffc7c99963f74033d22283a6d2ab3095f65cb49a17e",
+ "bd29703cf44551710ca14d091a6c98ee347931b2b8140faaaef2dbb40719df12",
+ "c96e7fadcf4e51f5c0dc03aeed33352d7f984c2d49791d173caad17d724b98155ff6b3dc6e082b90063434e9f85941c085dd8573fb4f23d"
+ "d0867615249e8e8c567ba74d4e6739919c46afc0a6b19b26c0e37e1810952dcb859b8a2df9ed322da89c4e7821166939809d2561980ff77"
+ "d3b797f1ecb1ed78e39614e096c72bed4587ac3229929ae4e164da9b00323410f8b17abed5cc8455656ee73114119e20b529294f8c578f7"
+ "f9492327ff40f9f1255abe84f7445c87c8b048e98eac746f6d58fb3261f61eb039e5e88da46c9fc5e35baeb0c1180e9913f49ab7aac5f59"
+ "76be1e384071470d80ddbf77c52e781f954d77978697cf555d1586469ce21ccd45f43283eeec3b976d6bd897f436ef9ccacde5da73f298b"
+ "d1b99c10e988befeb2988f8f03f96215a746590d35ff0a6a85fa1102d63a00cd71e3cb80753bfe98bf6744f2aec697993dab51cce21f823"
+ "656870",
+ "97b6b5b2082a66182783c0be6940ca9d63e931195b6cc84dbf9158e9b39834ac"},
+ {"message_with_special_chars", "f9fb473b9887e50ea38eef7380c82361432cd4b22c5f9b3700809990d8ed344c",
+ "2122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", "",
+ "bd29703cf44551710ca14d091a6c98ee347931b2b8140faaaef2dbb40719df12",
+ "b862c41221e3242c07c375dfded48e302aaebed7fbc91bbfe3b7c88345a58d3a13d83cfb87f08250b2d66f4590b5dd2bb2b08fabd4328d1"
+ "04f7b4e1bad80931c",
+ "7557d10f233e47a56f74a57458b5169e8cce5c4c98e3a3da02f6e49a3db4c2b3"},
+ {"message_with_unicode", "f9fb473b9887e50ea38eef7380c82361432cd4b22c5f9b3700809990d8ed344c",
+ "48656c6c6f2c20e4b896e7958c21", "", "bd29703cf44551710ca14d091a6c98ee347931b2b8140faaaef2dbb40719df12",
+ "cb0d460ca3daf8e3fd5623965b39b5c1de840e92d39f6caf4662b7a7983c53b29fe644bf45acea2644507ac01f0617a2",
+ "ebe8636326b11d90f9a670e63086e2fcd02b78c0aa5cacdb4f887e511d1ae4c9"},
+ };
+}
+
+} // namespace tde2e_core
diff --git a/tde2e/test/blockchain.cpp b/tde2e/test/blockchain.cpp
new file mode 100644
index 000000000..49397b088
--- /dev/null
+++ b/tde2e/test/blockchain.cpp
@@ -0,0 +1,251 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/e2e/TestBlockchain.h"
+
+#include "td/utils/simple_tests.h"
+#include "td/utils/Status.h"
+
+using namespace tde2e_core;
+using BB = BlockBuilder;
+using BT = BlockchainTester;
+
+S_TEST(BlockchainValidation, ZeroBlock) {
+ auto alice_pk = PrivateKey::generate().move_as_ok();
+ auto bob_pk = PrivateKey::generate().move_as_ok();
+ {
+ TEST_DEBUG_VALUE(description, "Valid: zero block with empty group state");
+ auto block = BB().with_height(0)
+ .with_block_hash({})
+ .with_group_state({{1, AllPermissions, alice_pk.to_public_key()}}, true, false)
+ .skip_group_state_proof()
+ .skip_shared_key_proof()
+ .build(alice_pk);
+ TEST_TRY_STATUS(BT().expect_ok(block));
+ }
+ {
+ TEST_DEBUG_VALUE(description, "Valid: zero block with group state only in proof");
+ auto block = BB().with_height(0)
+ .with_block_hash({})
+ .set_value("a", "b") // need some changes
+ .with_group_state({}, false, true, 7)
+ .with_shared_key({}, false, true)
+ .build(alice_pk);
+ TEST_TRY_STATUS(BT().expect_ok(block));
+ }
+ {
+ TEST_DEBUG_VALUE(description, "Invalid: zero block with wrong height");
+ auto block = BB().with_height(1)
+ .with_block_hash({})
+ .with_group_state({}, true, false)
+ .skip_group_state_proof()
+ .skip_shared_key_proof()
+ .build(alice_pk);
+ TEST_TRY_STATUS(BT().expect_error(E::InvalidBlock_HeightMismatch, block));
+ }
+ {
+ TEST_DEBUG_VALUE(description, "Invalid: zero block with wrong hash");
+ auto block = BB().with_height(0)
+ .with_block_hash({1})
+ .with_group_state({}, true, false)
+ .skip_group_state_proof()
+ .skip_shared_key_proof()
+ .build(alice_pk);
+ TEST_TRY_STATUS(BT().expect_error(E::InvalidBlock_HashMismatch, block));
+ }
+ {
+ TEST_DEBUG_VALUE(description, "Valid: zero block with invalid signature");
+ auto block = BB().with_height(0)
+ .with_block_hash({})
+ .with_group_state({}, true, false)
+ .skip_group_state_proof()
+ .skip_shared_key_proof()
+ .with_public_key(alice_pk)
+ .build_zero_sign();
+ TEST_TRY_STATUS(BT().expect_error(E::InvalidBlock_InvalidSignature, block));
+ }
+ {
+ TEST_DEBUG_VALUE(description, "Invalid: zero block with skipped group state proof");
+ auto block = BB().with_height(0)
+ .set_value("a", "b")
+ .with_block_hash({})
+ .skip_group_state_proof()
+ .skip_shared_key_proof()
+ .build(alice_pk);
+ TEST_DEBUG_VALUE(block, block);
+ TEST_TRY_STATUS(BT().expect_error(E::InvalidBlock_InvalidStateProof_Group, block));
+ }
+ {
+ TEST_DEBUG_VALUE(description, "Invalid: zero block with wrong user_id in group state proof");
+ auto block = BB().with_height(0)
+ .with_block_hash({})
+ .set_value("a", "b")
+ .with_group_state({{1, 3, alice_pk.to_public_key()}}, false, true)
+ .skip_shared_key_proof()
+ .build(alice_pk);
+ TEST_TRY_STATUS(BT().expect_error(E::InvalidBlock_InvalidStateProof_Group, block));
+ }
+ {
+ TEST_DEBUG_VALUE(description, "Invalid: zero block with other person in group state");
+ auto block = BB().with_height(0)
+ .with_block_hash({})
+ .with_group_state({{2, 3, bob_pk.to_public_key()}}, true, false)
+ .skip_group_state_proof()
+ .skip_shared_key_proof()
+ .build(alice_pk);
+ TEST_TRY_STATUS(BT().expect_error(E::InvalidBlock_NoPermissions, block));
+ }
+ {
+ TEST_DEBUG_VALUE(description, "Invalid: zero block with duplicate group state");
+ auto block = BB().with_height(0)
+ .with_block_hash({})
+ .with_group_state({{1, 3, alice_pk.to_public_key()}}, true, true)
+ .skip_shared_key_proof()
+ .build(alice_pk);
+ TEST_TRY_STATUS(BT().expect_error(E::InvalidBlock_InvalidStateProof_Group, block));
+ }
+ {
+ TEST_DEBUG_VALUE(description, "Invalid: duplicate user_id");
+ auto block = BB().with_height(0)
+ .with_block_hash({})
+ .with_group_state({{1, 1, alice_pk.to_public_key()}, {1, 1, bob_pk.to_public_key()}}, true, false)
+ .with_shared_key({1}, true, false)
+ .skip_group_state_proof()
+ .skip_shared_key_proof()
+ .build(alice_pk);
+ TEST_TRY_STATUS(BT().expect_error(E::InvalidBlock_InvalidGroupState, block));
+ }
+ {
+ TEST_DEBUG_VALUE(description, "Invalid: duplicate public key");
+ auto block =
+ BB().with_height(0)
+ .with_block_hash({})
+ .with_group_state({{1, 1, alice_pk.to_public_key()}, {2, 1, alice_pk.to_public_key()}}, true, false)
+ .with_shared_key({1}, true, false)
+ .skip_group_state_proof()
+ .skip_shared_key_proof()
+ .build(alice_pk);
+ TEST_TRY_STATUS(BT().expect_error(E::InvalidBlock_InvalidGroupState, block));
+ }
+ return td::Status::OK();
+}
+
+S_TEST(BlockchainValidation, GroupStateChanges) {
+ auto alice_pk = PrivateKey::generate().move_as_ok();
+ auto bob_pk = PrivateKey::generate().move_as_ok();
+ auto carol_pk = PrivateKey::generate().move_as_ok();
+ Block minus_one_block;
+ auto zero_block =
+ BB().with_previous_block(minus_one_block)
+ .with_group_state({{1, 1, alice_pk.to_public_key()}, {2, 2, bob_pk.to_public_key()}}, true, false, 3)
+ .with_shared_key({1}, true, false)
+ .skip_group_state_proof()
+ .skip_shared_key_proof()
+ .build(alice_pk);
+ {
+ TEST_DEBUG_VALUE(description, "Valid: sanity check of zero block");
+ BT bt;
+ TEST_TRY_STATUS(bt.expect_ok(zero_block));
+ }
+ {
+ TEST_DEBUG_VALUE(description, "Invalid: can't remove without permissions");
+ BT bt;
+ TEST_TRY_STATUS(bt.expect_ok(zero_block));
+ auto block = BB().with_previous_block(zero_block)
+ .with_group_state({{1, 1, alice_pk.to_public_key()}}, true, false)
+ .with_shared_key({1}, true, false)
+ .skip_group_state_proof()
+ .skip_shared_key_proof()
+ .build(alice_pk);
+ TEST_TRY_STATUS(bt.expect_error(E::InvalidBlock_NoPermissions, block));
+ }
+ {
+ TEST_DEBUG_VALUE(description, "Invalid: can't add without permissions");
+ BT bt;
+ TEST_TRY_STATUS(bt.expect_ok(zero_block));
+ auto block = BB().with_previous_block(zero_block)
+ .with_group_state({{3, 2, carol_pk.to_public_key()}}, true, false)
+ .with_shared_key({3}, true, false)
+ .skip_group_state_proof()
+ .skip_shared_key_proof()
+ .build(bob_pk);
+ TEST_TRY_STATUS(bt.expect_error(E::InvalidBlock_NoPermissions, block));
+ }
+ {
+ TEST_DEBUG_VALUE(description, "Invalid: can't raise permissions");
+ BT bt;
+ TEST_TRY_STATUS(bt.expect_ok(zero_block));
+ auto block = BB().with_previous_block(zero_block)
+ .with_group_state({{1, 3, alice_pk.to_public_key()}, {2, 2, bob_pk.to_public_key()}}, true, false)
+ .with_shared_key({1, 2}, true, false)
+ .skip_group_state_proof()
+ .skip_shared_key_proof()
+ .build(alice_pk);
+ TEST_TRY_STATUS(bt.expect_error(E::InvalidBlock_NoPermissions, block));
+ }
+ {
+ TEST_DEBUG_VALUE(description, "Valid: new shared key");
+ BT bt;
+ TEST_TRY_STATUS(bt.expect_ok(zero_block));
+ auto block = BB().with_previous_block(zero_block)
+ .with_group_state({{1, 1, alice_pk.to_public_key()}, {2, 2, bob_pk.to_public_key()}}, true, false)
+ .with_shared_key({1, 2}, true, false)
+ .skip_group_state_proof()
+ .skip_shared_key_proof()
+ .build(alice_pk);
+ TEST_TRY_STATUS(bt.expect_ok(block));
+ }
+ {
+ TEST_DEBUG_VALUE(description, "Invalid: remove self and change shared key");
+ BT bt;
+ TEST_TRY_STATUS(bt.expect_ok(zero_block));
+ auto block = BB().with_previous_block(zero_block)
+ .with_group_state({{1, 1, alice_pk.to_public_key()}}, true, false)
+ .with_shared_key({1}, true, false)
+ .skip_group_state_proof()
+ .skip_shared_key_proof()
+ .build(bob_pk);
+ TEST_TRY_STATUS(bt.expect_error(E::InvalidBlock_NoPermissions, block));
+ }
+ {
+ TEST_DEBUG_VALUE(description, "Valid: self join");
+ BT bt;
+ TEST_TRY_STATUS(bt.expect_ok(zero_block));
+ auto block =
+ BB().with_previous_block(zero_block)
+ .with_group_state(
+ {{1, 1, alice_pk.to_public_key()}, {2, 2, bob_pk.to_public_key()}, {3, 2, carol_pk.to_public_key()}},
+ true, false)
+ .with_shared_key({1, 2, 3}, true, false)
+ .skip_group_state_proof()
+ .skip_shared_key_proof()
+ .build(carol_pk);
+ TEST_TRY_STATUS(bt.expect_ok(block));
+ }
+ {
+ TEST_DEBUG_VALUE(description, "Invalid: self join when there is no permission");
+ BT bt;
+ auto zero_block_without_external =
+ BB().with_previous_block(minus_one_block)
+ .with_group_state({{1, 1, alice_pk.to_public_key()}, {2, 2, bob_pk.to_public_key()}}, true, false, 0)
+ .with_shared_key({1}, true, false)
+ .skip_group_state_proof()
+ .skip_shared_key_proof()
+ .build(alice_pk);
+ TEST_TRY_STATUS(bt.expect_ok(zero_block_without_external));
+ auto block =
+ BB().with_previous_block(zero_block_without_external)
+ .with_group_state(
+ {{1, 1, alice_pk.to_public_key()}, {2, 2, bob_pk.to_public_key()}, {3, 0, carol_pk.to_public_key()}},
+ true, false)
+ .with_shared_key({1, 2, 3}, true, false)
+ .skip_group_state_proof()
+ .skip_shared_key_proof()
+ .build(carol_pk);
+ TEST_TRY_STATUS(bt.expect_error(E::InvalidBlock_NoPermissions, block));
+ }
+ return td::Status::OK();
+}
diff --git a/tde2e/test/e2e.cpp b/tde2e/test/e2e.cpp
new file mode 100644
index 000000000..6ad625d06
--- /dev/null
+++ b/tde2e/test/e2e.cpp
@@ -0,0 +1,914 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/e2e/BitString.h"
+#include "td/e2e/Blockchain.h"
+#include "td/e2e/Call.h"
+#include "td/e2e/CheckSharedSecret.h"
+#include "td/e2e/Container.h"
+#include "td/e2e/DecryptedKey.h"
+#include "td/e2e/e2e_api.h"
+#include "td/e2e/EncryptedKey.h"
+#include "td/e2e/EncryptedStorage.h"
+#include "td/e2e/MessageEncryption.h"
+#include "td/e2e/Mnemonic.h"
+#include "td/e2e/QRHandshake.h"
+#include "td/e2e/TestBlockchain.h"
+#include "td/e2e/Trie.h"
+
+#include "td/telegram/e2e_api.h"
+
+#include "td/utils/base64.h"
+#include "td/utils/benchmark.h"
+#include "td/utils/common.h"
+#include "td/utils/crypto.h"
+#include "td/utils/Ed25519.h"
+#include "td/utils/FlatHashMap.h"
+#include "td/utils/format.h"
+#include "td/utils/logging.h"
+#include "td/utils/overloaded.h"
+#include "td/utils/Random.h"
+#include "td/utils/SharedSlice.h"
+#include "td/utils/simple_tests.h"
+#include "td/utils/Slice.h"
+#include "td/utils/SliceBuilder.h"
+#include "td/utils/Status.h"
+#include "td/utils/tests.h"
+#include "td/utils/tl_helpers.h"
+#include "td/utils/UInt.h"
+
+#include <map>
+#include <memory>
+#include <optional>
+#include <utility>
+
+using namespace tde2e_core;
+
+namespace api = tde2e_api;
+
+template <class T>
+static td::Status expect_error(td::Result<T> result) {
+ if (result.is_ok()) {
+ return td::Status::Error("Receive Ok instead of Error");
+ }
+ return td::Status::OK();
+}
+
+S_TEST(MessageEncryption, simple) {
+ std::string secret = "secret";
+ {
+ std::string data = "some private data";
+ std::string wrong_secret = "wrong secret";
+ auto encrypted_data = MessageEncryption::encrypt_data(data, secret);
+ LOG(ERROR) << encrypted_data.size();
+ TEST_TRY_RESULT(decrypted_data, MessageEncryption::decrypt_data(encrypted_data, secret));
+ TEST_ASSERT_EQ(data, decrypted_data, "decryption");
+ TEST_TRY_STATUS(expect_error(MessageEncryption::decrypt_data(encrypted_data, wrong_secret)));
+ TEST_TRY_STATUS(expect_error(MessageEncryption::decrypt_data("", secret)));
+ TEST_TRY_STATUS(expect_error(MessageEncryption::decrypt_data(std::string(32, 'a'), secret)));
+ TEST_TRY_STATUS(expect_error(MessageEncryption::decrypt_data(std::string(33, 'a'), secret)));
+ TEST_TRY_STATUS(expect_error(MessageEncryption::decrypt_data(std::string(64, 'a'), secret)));
+ TEST_TRY_STATUS(expect_error(MessageEncryption::decrypt_data(std::string(128, 'a'), secret)));
+ }
+
+ td::Random::Xorshift128plus rnd(123);
+ for (size_t i = 0; i < 255; i++) {
+ std::string data;
+ for (size_t j = 0; j < i; j++) {
+ data += static_cast<char>(rnd.fast('a', 'z'));
+ }
+ auto encrypted_data = MessageEncryption::encrypt_data(data, secret);
+ TEST_TRY_RESULT(decrypted_data, MessageEncryption::decrypt_data(encrypted_data, secret));
+ TEST_ASSERT_EQ(data, decrypted_data, "decryption");
+ }
+ return td::Status::OK();
+}
+
+struct E2eHandshakeTest {
+ td::Ed25519::PrivateKey alice;
+ td::Ed25519::PublicKey alice_public;
+ td::Ed25519::PrivateKey bob;
+ td::Ed25519::PublicKey bob_public;
+
+ td::SecureString shared_secret;
+};
+
+static E2eHandshakeTest gen_test() {
+ auto alice = td::Ed25519::generate_private_key().move_as_ok();
+ auto alice_public = alice.get_public_key().move_as_ok();
+ auto bob = td::Ed25519::generate_private_key().move_as_ok();
+ auto bob_public = bob.get_public_key().move_as_ok();
+ auto shared_secret = td::Ed25519::compute_shared_secret(alice.get_public_key().move_as_ok(), bob).move_as_ok();
+ return E2eHandshakeTest{std::move(alice), std::move(alice_public), std::move(bob), std::move(bob_public),
+ std::move(shared_secret)};
+}
+
+static void run_test(const E2eHandshakeTest &test) {
+ auto alice_secret =
+ td::Ed25519::compute_shared_secret(test.bob.get_public_key().move_as_ok(), test.alice).move_as_ok();
+ auto bob_secret = td::Ed25519::compute_shared_secret(test.alice.get_public_key().move_as_ok(), test.bob).move_as_ok();
+ CHECK(test.alice.get_public_key().move_as_ok().as_octet_string() == test.alice_public.as_octet_string());
+ CHECK(test.bob.get_public_key().move_as_ok().as_octet_string() == test.bob_public.as_octet_string());
+ CHECK(alice_secret == bob_secret);
+ CHECK(alice_secret == test.shared_secret);
+}
+
+static E2eHandshakeTest pregenerated_test() {
+ auto alice_public_key_str = td::base64url_decode_secure("RvG0CT5i8D-CYnfhp2akVC1tPRBIw-4X6ZqNBjH-mZI").move_as_ok();
+ auto alice_private_key_str = td::base64url_decode_secure("8NZGWKfRCJfiks74RG9_xHmYydarLiRsoq8VcJGPglg").move_as_ok();
+ auto bob_public_key_str = td::base64url_decode_secure("1V3BGwmbo-Mwsw7QlWKN4OZFPBP9z9VhFlZKRdzTrGw").move_as_ok();
+ auto bob_private_key_str = td::base64url_decode_secure("YMGoowtnZ99roUM2y5JRwiQrwGaNJ-ZRE5boy-l4aHg").move_as_ok();
+
+ auto alice_public_key = td::Ed25519::PublicKey(alice_public_key_str.copy());
+ auto alice_private_key = td::Ed25519::PrivateKey(alice_private_key_str.copy());
+ auto bob_public_key = td::Ed25519::PublicKey(bob_public_key_str.copy());
+ auto bob_private_key = td::Ed25519::PrivateKey(bob_private_key_str.copy());
+ auto shared_secret = td::base64url_decode_secure("CU6NsPBw59neM9crFvxKELbtKgAkI7G8tDHsb4CmyVA").move_as_ok();
+
+ return E2eHandshakeTest{std::move(alice_private_key), std::move(alice_public_key), std::move(bob_private_key),
+ std::move(bob_public_key), std::move(shared_secret)};
+}
+
+TEST(Handshake, InvalidKeys) {
+ auto private_key = td::Ed25519::generate_private_key().move_as_ok();
+ auto zero_key = td::Ed25519::PublicKey(td::SecureString(32, 0));
+ td::Ed25519::compute_shared_secret(zero_key, private_key).ensure_error();
+}
+
+TEST(Handshake, Random) {
+ auto test = gen_test();
+ run_test(test);
+}
+
+TEST(Handshake, Pregenerated) {
+ auto test = pregenerated_test();
+ run_test(test);
+}
+
+TEST(QRHandshake, Basic) {
+ td::int64 alice_user_id = 123;
+ td::int64 bob_user_id = 321;
+ auto alice_private_key = PrivateKey::generate().move_as_ok();
+ auto bob_private_key = PrivateKey::generate().move_as_ok();
+
+ auto bob = QRHandshakeBob::create(bob_user_id, bob_private_key);
+ auto start = bob.generate_start(); // should be passed via QR
+ auto alice =
+ QRHandshakeAlice::create(alice_user_id, alice_private_key, bob_user_id, bob_private_key.to_public_key(), start)
+ .move_as_ok();
+ auto accept = alice.generate_accept();
+ auto finish = bob.receive_accept(alice_user_id, alice_private_key.to_public_key(), accept).move_as_ok();
+ alice.receive_finish(finish).ensure();
+}
+
+TEST(CheckSharedSecret, Basic) {
+ auto alice = CheckSharedSecret::create();
+ auto bob = CheckSharedSecret::create();
+
+ alice.recive_commit_nonce(bob.commit_nonce()).ensure();
+ bob.recive_commit_nonce(alice.commit_nonce()).ensure();
+
+ alice.receive_reveal_nonce(bob.reveal_nonce().move_as_ok()).ensure();
+ bob.receive_reveal_nonce(alice.reveal_nonce().move_as_ok()).ensure();
+
+ CHECK(alice.finalize_hash("abc").move_as_ok() == bob.finalize_hash("abc").move_as_ok());
+}
+
+// node_type enum
+
+TEST(MiniBlockchain, Basic) {
+ auto private_key = PrivateKey::generate().move_as_ok();
+ Blockchain remote_blockchain = Blockchain::create_empty();
+ Blockchain local_blockchain = Blockchain::create_empty();
+
+ auto block = local_blockchain.set_value(std::string(32, 'a'), "b", private_key);
+ remote_blockchain.try_apply_block(block, {}).ensure();
+ local_blockchain.try_apply_block(block, {}).ensure();
+ block = local_blockchain.set_value(std::string(32, 'b'), "c", private_key);
+ remote_blockchain.try_apply_block(block, {}).ensure();
+ local_blockchain.try_apply_block(block, {}).ensure();
+}
+
+// Example usage
+TEST(Tree, BitString) {
+ LOG(ERROR) << "BitString count: " << BitString::get_counter_value();
+ td::UInt256 hash;
+ sha256("hello world", hash.as_mutable_slice());
+ BitString s(hash.as_slice());
+ for (auto l = 0; l <= 256; l++) {
+ for (auto r = l; r <= 256; r++) {
+ if (l > r) {
+ return;
+ }
+ auto a = s.substr(l, r - l);
+ BitString b;
+ auto str = td::serialize(a);
+ CHECK(str.size() % 4 == 0);
+
+ b = BitString::fetch_from_network(str).move_as_ok();
+ ASSERT_EQ(a, b);
+ }
+ }
+ LOG(ERROR) << "BitString count: " << BitString::get_counter_value();
+}
+
+TEST(Tree, SerializeStress) {
+ std::string value(32, 'a');
+ td::Random::Xorshift128plus rnd(123);
+ for (size_t i = 0; i < 10000; i++) {
+ size_t n = rnd.fast(0, 20);
+ TrieRef root = TrieNode::empty_node();
+ for (size_t j = 0; j < n; j++) {
+ td::UInt256 hash;
+ rnd.bytes(hash.as_mutable_slice());
+ root = set(root, hash.as_slice(), td::to_string(j)).move_as_ok();
+ }
+
+ auto old_hash = root->hash;
+ auto s = TrieNode::serialize_for_network(root).move_as_ok();
+ root = TrieNode::fetch_from_network(s).move_as_ok();
+ auto new_hash = root->hash;
+ CHECK(old_hash == new_hash);
+
+ auto snapshot = TrieNode::serialize_for_snapshot(root, "").move_as_ok();
+ auto snapshot_root = TrieNode::fetch_from_snapshot(snapshot).move_as_ok();
+ auto snapshot2 = TrieNode::serialize_for_snapshot(snapshot_root, snapshot).move_as_ok();
+ CHECK(snapshot == snapshot2);
+ }
+}
+
+TEST(Tree, BitStringCounter) {
+ CHECK(BitString::get_counter_value() == 0);
+ {
+ BitString bs(1);
+ size_t l = 1;
+ size_t r = 2;
+
+ auto a = bs.substr(l, r - l);
+ auto s = td::serialize(a);
+ CHECK(s.size() % 4 == 0);
+
+ BitString b = BitString::fetch_from_network(s).move_as_ok();
+ ASSERT_EQ(a, b);
+ }
+ CHECK(BitString::get_counter_value() == 0);
+}
+
+TEST(MerkleTree, Basic) {
+ // Build the tree
+ TrieRef root = TrieNode::empty_node();
+ root = set(root, "apple", "fruit").move_as_ok();
+ print_tree(root);
+ root = set(root, "application", "software").move_as_ok();
+ print_tree(root);
+ root = set(root, "banana", "fruit").move_as_ok();
+ print_tree(root);
+
+ ASSERT_EQ("fruit", get(root, "apple").move_as_ok());
+ ASSERT_EQ("software", get(root, "application").move_as_ok());
+ ASSERT_EQ("fruit", get(root, "banana").move_as_ok());
+
+ std::vector<td::Slice> keys = {"apple", "banana"};
+ TrieRef pruned_tree = generate_pruned_tree(root, keys).move_as_ok();
+ print_tree(pruned_tree);
+
+ ASSERT_EQ("fruit", get(pruned_tree, "apple").move_as_ok());
+ ASSERT_EQ("fruit", get(pruned_tree, "banana").move_as_ok());
+ get(pruned_tree, "application").ensure_error();
+
+ auto serialized = TrieNode::serialize_for_network(pruned_tree).move_as_ok();
+ TrieRef pruned_tree2 = TrieNode::fetch_from_network(serialized).move_as_ok();
+ print_tree(pruned_tree2);
+
+ ASSERT_EQ("fruit", get(pruned_tree2, "apple").move_as_ok());
+ ASSERT_EQ("fruit", get(pruned_tree2, "banana").move_as_ok());
+ get(pruned_tree2, "application").ensure_error();
+}
+
+static TrieRef root;
+static const int N = 1'000'000;
+
+TEST(Tree, BenchA) {
+ LOG(ERROR) << "BitString count: " << BitString::get_counter_value();
+ root = TrieNode::empty_node();
+ std::string value(32, 'a');
+ for (int i = 0; i < N; i++) {
+ auto key = value + std::to_string(i);
+ td::UInt256 hash;
+ sha256(key, hash.as_mutable_slice());
+ root = set(std::move(root), hash.as_slice().str(), value).move_as_ok();
+ }
+ LOG(ERROR) << "BitString count: " << BitString::get_counter_value();
+}
+
+static std::string serialized_root;
+
+TEST(Tree, Serialize) {
+ serialized_root = TrieNode::serialize_for_network(root).move_as_ok();
+}
+
+TEST(Tree, Clear) {
+ root = {};
+ LOG(ERROR) << "BitString count: " << BitString::get_counter_value();
+}
+
+TEST(Tree, Deserialize) {
+ root = TrieNode::fetch_from_network(serialized_root).move_as_ok();
+ LOG(ERROR) << "BitString count: " << BitString::get_counter_value();
+}
+
+TEST(Tree, BenchAPruned) {
+ std::string value(32, 'a');
+ size_t step = 1;
+ std::vector<td::UInt256> keys_str(step);
+ std::vector<td::Slice> keys(step);
+ for (size_t i = 0; i < 1000000; i += step) {
+ for (size_t j = 0; j < step; j++) {
+ auto key = value + std::to_string((i + j) % N);
+ sha256(key, keys_str[j].as_mutable_slice());
+ keys[j] = keys_str[j].as_slice();
+ }
+ //auto node = generate_pruned_tree(TrieNode::fetch_from_snapshot(serialized_root).move_as_ok(), keys, serialized_root).move_as_ok();
+ auto node = generate_pruned_tree(root, keys, serialized_root).move_as_ok();
+ auto x = TrieNode::serialize_for_network(node).move_as_ok();
+ LOG_IF(ERROR, i == 0) << x.size() << " bytes serialized";
+ }
+}
+
+TEST(Tree, BenchAA) {
+ std::string value(32, 'a');
+ for (int i = 0; i < N; i++) {
+ auto key = value + std::to_string(i);
+ td::UInt256 hash;
+ sha256(key, hash.as_mutable_slice());
+ CHECK(value == get(root, hash.as_slice().str()).move_as_ok());
+ }
+}
+TEST(Tree, BenchAAA) {
+ std::string value(32, 'a');
+ for (int i = 0; i < N; i++) {
+ auto key = value + std::to_string(i);
+ td::UInt256 hash;
+ sha256(key, hash.as_mutable_slice());
+ root = set(std::move(root), hash.as_slice().str(), value).move_as_ok();
+ }
+}
+
+static td::FlatHashMap<std::string, std::string> map;
+
+TEST(Tree, BenchB) {
+ std::string value(32, 'a');
+ for (int i = 0; i < N; i++) {
+ auto key = value + std::to_string(i);
+ td::UInt256 hash;
+ sha256(key, hash.as_mutable_slice());
+ map.emplace(hash.as_slice().str(), value);
+ }
+}
+
+TEST(Tree, BenchBB) {
+ std::string value(32, 'a');
+ for (int i = 0; i < N; i++) {
+ auto key = value + std::to_string(i);
+ td::UInt256 hash;
+ sha256(key, hash.as_mutable_slice());
+ CHECK(value == map.find(hash.as_slice().str())->second);
+ }
+}
+
+TEST(Tree, BenchBBB) {
+ std::string value(32, 'a');
+ for (int i = 0; i < N; i++) {
+ auto key = value + std::to_string(i);
+ td::UInt256 hash;
+ sha256(key, hash.as_mutable_slice());
+ map.emplace(hash.as_slice().str(), value);
+ }
+}
+
+// TODO: to we need both secret and local password?..
+// user_password is known only to user and never stored
+// secret used to decrypt the key and should be stored somewhere safe
+// encrypted_data could be stored anywhere
+//
+// Is it too complicated? Is it necessary to encrypt
+static td::Result<EncryptedKey> create_new_encrypted_key(td::Slice user_password) {
+ TRY_RESULT(mnemonic, Mnemonic::create_new());
+ auto private_key = mnemonic.to_private_key();
+ auto decrypted_key = DecryptedKey(mnemonic.get_words(), std::move(private_key));
+ return decrypted_key.encrypt(user_password);
+}
+
+static td::Result<EncryptedKey> change_user_password(const EncryptedKey &encrypted_key, td::Slice user_password,
+ td::Slice new_user_password) {
+ TRY_RESULT(decrypted_key, encrypted_key.decrypt(user_password, false));
+ return decrypted_key.encrypt(new_user_password);
+}
+
+static td::Result<td::SecureString> export_mnemonic(const EncryptedKey &encrypted_key, td::Slice user_password) {
+ TRY_RESULT(decrypted_key, encrypted_key.decrypt(user_password, false));
+ CHECK(decrypted_key.mnemonic_words.size() == 24);
+ size_t length = decrypted_key.mnemonic_words.size() - 1;
+ for (auto &word : decrypted_key.mnemonic_words) {
+ length += word.size();
+ }
+ td::SecureString res(length);
+ auto dest = res.as_mutable_slice();
+ bool is_first = true;
+ for (auto &word : decrypted_key.mnemonic_words) {
+ if (!is_first) {
+ dest[0] = ' ';
+ dest.remove_prefix(1);
+ } else {
+ is_first = false;
+ }
+ dest.copy_from(word);
+ dest.remove_prefix(word.size());
+ }
+ return res;
+}
+
+static td::Result<EncryptedKey> import_mnemonic(td::Slice mnemonic_words, td::Slice user_password) {
+ TRY_RESULT(mnemonic, Mnemonic::create(td::SecureString(mnemonic_words), td::SecureString()));
+ auto decrypted_key = DecryptedKey(mnemonic);
+ return decrypted_key.encrypt(user_password);
+}
+
+TEST(E2E, GenerateKeys) {
+ // generate key
+ auto encrypted_key = create_new_encrypted_key("user_password").move_as_ok();
+ change_user_password(encrypted_key, "bad_user_password", "user_password").ensure_error();
+ auto new_encrypted_key = change_user_password(encrypted_key, "user_password", "new_password").move_as_ok();
+ export_mnemonic(new_encrypted_key, "user_password").ensure_error();
+ auto mnemonic = export_mnemonic(new_encrypted_key, "new_password").move_as_ok();
+ auto other_encrypted_key = import_mnemonic(mnemonic, "new_password").move_as_ok();
+ CHECK(encrypted_key.o_public_key == new_encrypted_key.o_public_key);
+}
+
+TEST(E2E_API, Key) {
+ using namespace tde2e_api;
+ auto alice_pk = key_generate_private_key().value();
+ auto bob_pk = key_generate_private_key().value();
+ auto carol_pk = key_generate_private_key().value();
+
+ auto secret = key_from_bytes("secret").value();
+ auto bad_secret = key_from_bytes("bad_secret").value();
+
+ auto encrypted_alice_pk = key_to_encrypted_private_key(alice_pk, secret).value();
+ key_from_encrypted_private_key(encrypted_alice_pk, bad_secret).error();
+ auto alice_pk_copy = key_from_encrypted_private_key(encrypted_alice_pk, secret).value();
+
+ ASSERT_EQ(key_to_public_key(alice_pk).value(), key_to_public_key(alice_pk_copy).value());
+ auto alice_PK = key_from_public_key(key_to_public_key(alice_pk).value()).value();
+ auto bob_PK = key_from_public_key(key_to_public_key(bob_pk).value()).value();
+ auto carol_PK = key_from_public_key(key_to_public_key(carol_pk).value()).value();
+
+ key_destroy(alice_pk_copy).value();
+ key_to_public_key(alice_pk_copy).error();
+
+ auto words = key_to_words(alice_pk).value();
+ ASSERT_EQ(alice_pk, key_from_words(std::move(words)).value());
+
+ auto shared_key_ab = key_from_ecdh(alice_pk, bob_PK).value();
+ auto shared_key_ba = key_from_ecdh(bob_pk, alice_PK).value();
+ auto shared_key_ac = key_from_ecdh(alice_pk, carol_PK).value();
+
+ auto encrypted = encrypt_message_for_many({shared_key_ab, shared_key_ac}, "very secret message").value();
+ ASSERT_EQ(
+ "very secret message",
+ decrypt_message_for_many(shared_key_ba, encrypted.encrypted_headers[0], encrypted.encrypted_message).value());
+ decrypt_message_for_many(shared_key_ac, encrypted.encrypted_headers[0], encrypted.encrypted_message).error();
+
+ auto encrypted2 = encrypt_message_for_one(shared_key_ab, "very secret message").value();
+ ASSERT_EQ("very secret message", decrypt_message_for_one(shared_key_ba, encrypted2).value());
+ decrypt_message_for_one(shared_key_ac, encrypted2).error();
+ key_destroy_all();
+}
+
+TEST(E2E_API, HandshakeVerify) {
+ using namespace tde2e_api;
+ auto bob_id = 123;
+ auto alice_id = 321;
+ auto alice_pk = key_generate_private_key().value();
+ auto bob_pk = key_generate_private_key().value();
+
+ // Bob creates handshake
+ auto bob_handshake_id = handshake_create_for_bob(bob_id, bob_pk).value();
+ // Start is shown on QR
+ auto start = handshake_bob_send_start(bob_handshake_id).value();
+
+ // Alice received qr, received information about qr from server and create handshake
+ auto alice_handshake_id =
+ handshake_create_for_alice(alice_id, alice_pk, bob_id, key_to_public_key(bob_pk).value(), start).value();
+ auto accept = handshake_alice_send_accept(alice_handshake_id).value();
+ // Alice knows shared key. She knows that it is known to author of QR code, but not necessary to bob_pk owner.
+ auto shared_a = handshake_get_shared_key_id(alice_handshake_id).value();
+
+ // Bob receives accept and generates finish
+ auto finish =
+ handshake_bob_receive_accept_send_finish(bob_handshake_id, alice_id, key_to_public_key(alice_pk).value(), accept)
+ .value();
+ // At this point Bob "verified" Alice
+ // Bob knows shared key
+ auto shared_b = handshake_get_shared_key_id(bob_handshake_id).value();
+
+ // Alice receives and verifies finish
+ handshake_alice_receive_finish(alice_handshake_id, finish).value();
+ // At this point Alice "verified" Bob
+
+ ASSERT_EQ(shared_a, shared_b);
+ handshake_destroy_all();
+}
+
+TEST(E2E_API, HandshakeLogin) {
+ using namespace tde2e_api;
+
+ auto alice_id = 321;
+ auto alice_pk = key_generate_private_key().value();
+
+ auto bob_login_id = login_create_for_bob().value();
+ auto start = login_bob_send_start(bob_login_id).value();
+ auto alice_data = login_create_for_alice(alice_id, alice_pk, start).value();
+ auto received_alice_pk =
+ login_finish_for_bob(bob_login_id, alice_id, key_to_public_key(alice_pk).value(), alice_data).value();
+ ASSERT_EQ(key_to_public_key(alice_pk).value(), key_to_public_key(received_alice_pk).value());
+ login_destroy_all();
+}
+
+TEST(Container, Basic) {
+ using namespace tde2e_api;
+ Container<TypeInfo<int, false, false>, TypeInfo<std::string, false, true>, TypeInfo<std::vector<int>, true, false>,
+ TypeInfo<std::vector<std::string>, true, true>>
+ container;
+
+ auto id_int = container.emplace<int>(1);
+ td::UInt256 hash;
+ hash.as_mutable_slice().fill(7);
+ auto id_string = container.try_emplace<std::string>(hash, "hello");
+ auto id_string_2 = container
+ .try_build<std::string>(hash,
+ []() -> td::Result<std::string> {
+ UNREACHABLE();
+ return "...";
+ })
+ .move_as_ok();
+ ASSERT_EQ(id_string, id_string_2);
+ auto id_vec_int = container
+ .try_build<std::vector<int>>({},
+ []() -> td::Result<std::vector<int>> {
+ return std::vector<int>{1, 2, 3, 4};
+ })
+ .move_as_ok();
+ auto id_vec_string = container.emplace<std::vector<std::string>>(std::vector<std::string>{"a", "b", "c"});
+
+ container.get_shared<int>(id_int).ensure();
+ container.get_shared<std::string>(id_string).ensure();
+ container.get_unique<std::vector<int>>(id_vec_int).ensure();
+ container.get_unique<std::vector<std::string>>(id_vec_string).ensure();
+
+ container.get_shared<int>(id_string).ensure_error();
+ container.get_shared<std::string>(id_int).ensure_error();
+ container.get_unique<std::vector<int>>(id_vec_string).ensure_error();
+ container.get_unique<std::vector<std::string>>(id_vec_int).ensure_error();
+}
+
+struct BaselineBlockchainState {
+ std::map<std::string, std::string> key_value_state;
+ GroupStateRef group_state;
+ GroupSharedKeyRef shared_key;
+ td::int32 height{-1};
+ std::string get_value(const std::string &key) const {
+ auto it = key_value_state.find(key);
+ if (it == key_value_state.end()) {
+ return "";
+ }
+ return it->second;
+ }
+ void apply_changes(const std::vector<Change> &changes) {
+ for (const auto &change_v : changes) {
+ std::visit(td::overloaded([&](const ChangeNoop &) {},
+ [&](const ChangeSetValue &change) { key_value_state[change.key] = change.value; },
+ [&](const ChangeSetGroupState &change) { group_state = change.group_state; },
+ [&](const ChangeSetSharedKey &change) { shared_key = change.shared_key; }),
+ change_v.value);
+ }
+ height++;
+ }
+};
+
+S_TEST(E2E_Blockchain, Base) {
+ TEST_TRY_RESULT(pk, PrivateKey::generate());
+ TEST_TRY_RESULT(pk2, PrivateKey::generate());
+
+ BlockchainTester tester;
+
+ auto to_hash = [](td::Slice key) {
+ std::string res(32, 0);
+ td::sha256(key, res);
+ return res;
+ };
+
+ auto a = to_hash("a");
+ auto b = to_hash("b");
+
+ TEST_ASSERT_EQ("", tester.get_value(a), "empty blockchain");
+ TEST_ASSERT_EQ("", tester.get_value(b), "empty blockchain");
+ using BB = BlockBuilder;
+
+ TEST_TRY_STATUS(
+ tester.expect_ok({BB::make_set_value(a, "hello a"),
+ BB::make_group_change({{2, GroupParticipantFlags::AllPermissions, pk2.to_public_key()}})},
+ pk2));
+ TEST_ASSERT_EQ("hello a", tester.get_value(a), "hello a");
+ TEST_TRY_STATUS(tester.expect_error(E::Any, {BB::make_set_value(a, "hello b")}, pk));
+ TEST_TRY_STATUS(tester.expect_ok({BB::make_set_value(a, "hello b")}, pk2));
+ TEST_ASSERT_EQ("hello b", tester.get_value(a), "...");
+ tester.reindex();
+ TEST_ASSERT_EQ("hello b", tester.get_value(a), "...");
+ return td::Status::OK();
+}
+
+S_TEST(E2E_Blockchain, Stress) {
+ BlockchainTester tester;
+ TEST_TRY_RESULT(pk, PrivateKey::generate());
+
+ td::Random::Xorshift128plus rnd(123);
+ auto gen_string = [&](auto from, auto to, auto size) {
+ std::string s(size, 0);
+ for (auto &c : s) {
+ c = static_cast<char>(rnd.fast(from, to));
+ }
+ return s;
+ };
+
+ auto to_hash = [](td::Slice key) {
+ std::string res(32, 0);
+ td::sha256(key, res);
+ return res;
+ };
+ auto gen_key = [&] {
+ auto len = rnd.fast(1, 15);
+ return to_hash(gen_string('a', 'b', len));
+ };
+
+ auto gen_value = [&] {
+ std::string res(rnd.fast(1, 64), 0);
+ rnd.bytes(res);
+ return res;
+ };
+
+ auto gen_query = [&] {
+ std::vector<std::string> keys(rnd.fast(1, 1));
+ for (auto &key : keys) {
+ key = gen_key();
+ };
+ return keys;
+ };
+
+ auto gen_changes = [&] {
+ auto n = rnd.fast(1, 2);
+ std::vector<Change> changes;
+ changes.reserve(n);
+ for (int i = 0; i < n; i++) {
+ changes.push_back(BlockBuilder::make_set_value(gen_key(), gen_value()));
+ }
+ return changes;
+ };
+
+ auto run_get = [&] {
+ auto keys = gen_query();
+ TEST_TRY_STATUS(tester.get_values(keys));
+ return td::Status::OK();
+ };
+
+ auto run_set = [&] {
+ auto changes = gen_changes();
+ TEST_TRY_STATUS(tester.apply(changes, pk));
+ return td::Status::OK();
+ };
+
+ auto reindex = [&] {
+ tester.reindex();
+ return td::Status::OK();
+ };
+
+ td::RandomSteps steps{{{run_set, 10}, {run_get, 100}, {reindex, 1}}};
+ for (size_t i = 0; i < 10000; i++) {
+ steps.step(rnd);
+ }
+ return td::Status::OK();
+}
+
+using namespace tde2e_api;
+S_TEST(E2E_Blockchain, Call) {
+ SET_VERBOSITY_LEVEL(3);
+ CallTester ct;
+ TEST_TRY_STATUS(ct.start_call({0, 1, 2}));
+ TEST_TRY_STATUS(ct.check_shared_key());
+ TEST_TRY_STATUS(ct.check_emoji_hash());
+ TEST_TRY_STATUS(ct.update_call(0, {0, 3, 4, 5}));
+ TEST_TRY_STATUS(ct.check_shared_key());
+ TEST_TRY_STATUS(ct.check_emoji_hash());
+ return td::Status::OK();
+}
+
+TEST(Call, Basic_API) {
+ using namespace tde2e_api;
+ auto F = [](Result<std::string> block) -> Result<std::string> {
+ if (block.is_ok()) {
+ return Blockchain::from_local_to_server(block.value());
+ }
+ return block;
+ };
+
+ auto key0 = key_generate_temporary_private_key().value();
+ auto pkey0 = key_from_public_key(key_to_public_key(key0).value()).value();
+ auto key1 = key_generate_temporary_private_key().value();
+ auto pkey1 = key_from_public_key(key_to_public_key(key1).value()).value();
+ auto key2 = key_generate_temporary_private_key().value();
+ auto pkey2 = key_from_public_key(key_to_public_key(key2).value()).value();
+ auto key3 = key_generate_temporary_private_key().value();
+ auto pkey3 = key_from_public_key(key_to_public_key(key3).value()).value();
+
+ auto zero_block = F(call_create_zero_block(key0, CallState{0, {CallParticipant{-1, pkey0, 3}}})).value();
+
+ auto call1 = call_create(-1, key0, zero_block).value();
+ auto block0 = F(call_create_self_add_block(key1, zero_block, CallParticipant{1, pkey1, 3})).value();
+ call1 = call_create(1, key1, block0).value();
+
+ auto block1 = F(call_create_self_add_block(key2, block0, CallParticipant{2, pkey2, 3})).value();
+ call_apply_block(call1, block1).value();
+ auto call2 = call_create(2, key2, block1).value();
+ ASSERT_EQ(call_get_verification_words(call2).value().words, call_get_verification_words(call1).value().words);
+
+ auto block2 = F(call_create_change_state_block(
+ call2, CallState{0, {CallParticipant{2, pkey2, 3}, CallParticipant{3, pkey3, 3}}}))
+ .value();
+ call_describe_block(block2).value();
+ auto call3 = call_create(3, key3, block2).value();
+
+ call_apply_block(call2, block2).value();
+ CHECK(!call_apply_block(call1, block2).is_ok());
+
+ // call2 and call3 verification
+ ASSERT_EQ(call_get_verification_words(call2).value().words, call_get_verification_words(call3).value().words);
+
+ auto block31 = F(call_create_change_state_block(
+ call2, CallState{0, {CallParticipant{2, pkey2, 3}, CallParticipant{3, pkey3, 3}}}))
+ .value();
+
+ call_apply_block(call2, block31).value();
+ auto commit2 = F(call_pull_outbound_messages(call2).value().at(0)).value();
+
+ call_describe_message(commit2).value();
+
+ call_receive_inbound_message(call2, commit2).value();
+ call_receive_inbound_message(call3, commit2).value();
+
+ call_apply_block(call3, block31).value();
+ auto commit3 = F(call_pull_outbound_messages(call3).value().at(0)).value();
+
+ CHECK(commit2 != commit3);
+ call_receive_inbound_message(call2, commit3).value();
+ call_receive_inbound_message(call3, commit3).value();
+
+ auto reveal2 = F(call_pull_outbound_messages(call2).value().at(0)).value();
+ auto reveal3 = F(call_pull_outbound_messages(call3).value().at(0)).value();
+ call_receive_inbound_message(call2, reveal2).value();
+ call_receive_inbound_message(call2, reveal3).value();
+ call_receive_inbound_message(call3, reveal2).value();
+ call_receive_inbound_message(call3, reveal3).value();
+
+ ASSERT_EQ(call_get_verification_state(call2).value().emoji_hash.value(),
+ call_get_verification_state(call3).value().emoji_hash.value());
+
+ auto e = call_encrypt(call2, 1, "hello").value();
+ auto e2 = call_encrypt(call2, 1, "hello").value();
+ CHECK(e != "hello");
+ LOG(ERROR) << e.size();
+ ASSERT_TRUE(!call_decrypt(call2, 2, 1, e).is_ok());
+ // ASSERT_TRUE(!call_decrypt(call3, 2, 2, e).is_ok()); Uncomment if we will validate channel_id
+ ASSERT_TRUE(!call_decrypt(call3, 1, 1, e).is_ok());
+ ASSERT_EQ("hello", call_decrypt(call3, 2, 1, e).value());
+ ASSERT_TRUE(!call_decrypt(call3, 2, 1, e).is_ok());
+
+ auto block3 = F(call_create_change_state_block(
+ call2, CallState{0, {CallParticipant{2, pkey2, 3}, CallParticipant{3, pkey3, 3}}}))
+ .value();
+ call_apply_block(call3, block3).value();
+ ASSERT_TRUE(!call_decrypt(call3, 2, 1, e).is_ok());
+ ASSERT_EQ("hello", call_decrypt(call3, 2, 1, e2).value());
+ ASSERT_TRUE(call_decrypt(call2, 3, 1, call_encrypt(call3, 1, "bye").value()).is_ok());
+
+ LOG(ERROR) << call_describe(call1).value();
+ LOG(ERROR) << call_describe(call2).value();
+
+ key_destroy_all();
+ call_destroy_all();
+}
+
+TEST(State, Basic) {
+ SET_VERBOSITY_LEVEL(3);
+ using namespace tde2e_api;
+
+ ServerBlockchain kv_server;
+ auto pk = key_generate_private_key().value();
+ auto storage = storage_create(pk, {}).value();
+
+ auto contact_pk = key_generate_private_key().value();
+ auto contact_public_key = key_from_public_key(key_to_public_key(contact_pk).value()).value();
+
+ storage_get_contact(storage, contact_public_key);
+ storage_get_contact(storage, contact_public_key).error();
+ Entry<Name> entry_name;
+ entry_name.value = Name{"A", "B"};
+ auto signed_entry_name = storage_sign_entry(contact_pk, entry_name).value();
+ auto update_id = storage_update_contact(storage, contact_public_key, signed_entry_name).value();
+ (void)update_id;
+
+ auto load_proofs = [&] {
+ auto keys = storage_get_blockchain_state(storage).value().required_proofs;
+ auto proof = kv_server.get_proof(storage_blockchain_height(storage).value(), keys).move_as_ok();
+ storage_blockchain_add_proof(storage, proof, keys).value();
+ };
+ auto update_blockchain = [&] {
+ auto block = storage_get_blockchain_state(storage).value().next_suggested_block;
+ if (block.empty()) {
+ return;
+ }
+ kv_server.try_apply_block(block).ensure();
+ storage_blockchain_apply_block(storage, block).value();
+ };
+
+ load_proofs();
+
+ Value value;
+ value.o_name = entry_name;
+
+ ASSERT_EQ(std::optional<Value>(), storage_get_contact(storage, contact_public_key).value());
+ ASSERT_EQ(value, storage_get_contact_optimistic(storage, contact_public_key).value());
+
+ update_blockchain();
+ ASSERT_EQ(value, storage_get_contact(storage, contact_public_key).value());
+}
+
+class CallEncryptionBench final : public td::Benchmark {
+ public:
+ explicit CallEncryptionBench(size_t msg_size) : msg_size_(msg_size) {
+ msg_ = std::string(msg_size_, '\1');
+ }
+ std::string get_description() const final {
+ return PSTRING() << "Call encrypt/decrypt msg_size=" << td::format::as_size(msg_size_);
+ }
+
+ void start_up() final {
+ auto pk1 = PrivateKey::generate().move_as_ok();
+ auto pk2 = PrivateKey::generate().move_as_ok();
+ auto group = std::make_shared<GroupState>(
+ GroupState{{GroupParticipant{1, 0, pk1.to_public_key()}, GroupParticipant{2, 0, pk2.to_public_key()}}});
+ td::SecureString shared_key(32, '\0');
+ e1_ = std::make_unique<CallEncryption>(1, pk1);
+ e1_->add_shared_key(1, td::UInt256{}, shared_key.copy(), group);
+ e2_ = std::make_unique<CallEncryption>(2, pk2);
+ e2_->add_shared_key(1, td::UInt256{}, shared_key.copy(), group);
+ }
+
+ void run(int n) final {
+ for (int i = 0; i < n; i++) {
+ auto encrypted = e1_->encrypt(1, msg_).move_as_ok();
+ CHECK(msg_ == e2_->decrypt(1, 1, encrypted).move_as_ok());
+ }
+ }
+
+ private:
+ size_t msg_size_{};
+ std::string msg_;
+ std::unique_ptr<CallEncryption> e1_;
+ std::unique_ptr<CallEncryption> e2_;
+};
+
+TEST(Call, Bench) {
+ td::bench(CallEncryptionBench(16));
+ td::bench(CallEncryptionBench(1024));
+ td::bench(CallEncryptionBench(16 * 1024));
+ td::bench(CallEncryptionBench(64 * 1024));
+}
+
+TEST(Keys, Sanity) {
+ auto pk = PrivateKey::generate().move_as_ok();
+ auto hello_sign = pk.sign("hello").move_as_ok();
+ pk.to_public_key().verify("hello", hello_sign).ensure();
+ auto bad_sign = hello_sign.to_u512();
+ bad_sign.raw[0]++;
+ pk.to_public_key().verify("hello", Signature::from_u512(bad_sign)).ensure_error();
+}
+
+#if TG_ENGINE
+int main() {
+ td::TestsRunner::get_default().run_all();
+ _Exit(0);
+}
+#endif
diff --git a/tde2e/test/encryption.cpp b/tde2e/test/encryption.cpp
new file mode 100644
index 000000000..9382aef0f
--- /dev/null
+++ b/tde2e/test/encryption.cpp
@@ -0,0 +1,65 @@
+//
+// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+#include "td/e2e/MessageEncryption.h"
+
+#include "EncryptionTestVectors.h"
+
+#include "td/utils/logging.h"
+#include "td/utils/misc.h"
+#include "td/utils/SharedSlice.h"
+#include "td/utils/simple_tests.h"
+#include "td/utils/Slice.h"
+#include "td/utils/Status.h"
+#include "td/utils/tests.h"
+
+namespace tde2e_core {
+class EncryptionTest {
+ public:
+ static td::SecureString encrypt_data_with_deterministic_padding(td::Slice data, td::Slice secret, td::Slice extra) {
+ auto prefix = MessageEncryption::gen_deterministic_prefix(data.size(), 16);
+ td::SecureString combined(prefix.size() + data.size());
+ combined.as_mutable_slice().copy_from(prefix);
+ combined.as_mutable_slice().substr(prefix.size()).copy_from(data);
+ return MessageEncryption::encrypt_data_with_prefix(combined.as_slice(), secret, extra);
+ }
+};
+} // namespace tde2e_core
+
+using namespace tde2e_core;
+
+S_TEST(EncryptionTest, test_vectors) {
+ auto test_vectors = get_test_vectors();
+ for (const auto &vec : test_vectors) {
+ LOG(INFO) << "Testing vector: " << vec.name;
+
+ // Convert hex strings to binary
+ auto secret = td::hex_decode(vec.secret).move_as_ok();
+ auto data = td::hex_decode(vec.data).move_as_ok();
+ auto extra = td::hex_decode(vec.extra).move_as_ok();
+ auto header = td::hex_decode(vec.header).move_as_ok();
+ auto expected_encrypted = td::hex_decode(vec.encrypted).move_as_ok();
+ auto expected_encrypted_header = td::hex_decode(vec.encrypted_header).move_as_ok();
+
+ // Test encrypt_data with deterministic padding
+ auto encrypted = EncryptionTest::encrypt_data_with_deterministic_padding(data, secret, extra);
+
+ // Test encrypt_header
+ auto encrypted_header_result = MessageEncryption::encrypt_header(header, encrypted, secret);
+ ASSERT_TRUE(encrypted_header_result.is_ok());
+
+ // For simplicity during debugging, verify only decryption
+ auto decrypted_result = MessageEncryption::decrypt_data(expected_encrypted, secret, extra);
+ ASSERT_TRUE(decrypted_result.is_ok());
+ ASSERT_EQ(td::hex_encode(decrypted_result.ok()), vec.data);
+
+ auto decrypted_header_result =
+ MessageEncryption::decrypt_header(expected_encrypted_header, expected_encrypted, secret);
+ ASSERT_TRUE(decrypted_header_result.is_ok());
+ ASSERT_EQ(td::hex_encode(decrypted_header_result.ok()), vec.header);
+ }
+ return td::Status::OK();
+}