aboutsummaryrefslogtreecommitdiffhomepage
path: root/tde2e/td/e2e/Blockchain.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tde2e/td/e2e/Blockchain.cpp')
-rw-r--r--tde2e/td/e2e/Blockchain.cpp821
1 files changed, 821 insertions, 0 deletions
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