зеркало из https://github.com/microsoft/CCF.git
Aft: In BFT mode propagate backup signatures and acks to signatures (#1658)
This commit is contained in:
Родитель
6b86b17338
Коммит
5d9cfc65f1
|
@ -319,6 +319,16 @@ if(BUILD_TESTS)
|
|||
http_parser.host
|
||||
)
|
||||
|
||||
add_unit_test(
|
||||
progress_tracker_test
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/progress_tracker.cpp
|
||||
)
|
||||
target_include_directories(progress_tracker_test PRIVATE ${EVERCRYPT_INC})
|
||||
target_link_libraries(
|
||||
progress_tracker_test PRIVATE ${CRYPTO_LIBRARY} evercrypt.host
|
||||
secp256k1.host
|
||||
)
|
||||
|
||||
add_unit_test(
|
||||
secret_sharing_test
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/secret_share.cpp
|
||||
|
|
|
@ -334,6 +334,11 @@ namespace aft
|
|||
return configurations.back().nodes;
|
||||
}
|
||||
|
||||
uint32_t node_count() const
|
||||
{
|
||||
return get_latest_configuration().size();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool replicate(
|
||||
const std::vector<std::tuple<Index, T, bool>>& entries, Term term)
|
||||
|
@ -456,6 +461,14 @@ namespace aft
|
|||
recv_request_vote_response(data, size);
|
||||
break;
|
||||
|
||||
case bft_signature_received_ack:
|
||||
recv_signature_received_ack(data, size);
|
||||
break;
|
||||
|
||||
case bft_nonce_reveal:
|
||||
recv_nonce_reveal(data, size);
|
||||
break;
|
||||
|
||||
default:
|
||||
{
|
||||
}
|
||||
|
@ -845,22 +858,42 @@ namespace aft
|
|||
to,
|
||||
state->last_idx);
|
||||
|
||||
auto progress_tracker = store->get_progress_tracker();
|
||||
CCF_ASSERT(progress_tracker != nullptr, "progress_tracker is not set");
|
||||
auto h = progress_tracker->get_my_hashed_nonce(
|
||||
state->current_view, state->last_idx);
|
||||
|
||||
Nonce hashed_nonce;
|
||||
std::copy(h.begin(), h.end(), hashed_nonce.begin());
|
||||
|
||||
SignedAppendEntriesResponse r = {
|
||||
{raft_append_entries_signed_response, state->my_node_id},
|
||||
state->current_view,
|
||||
state->last_idx,
|
||||
hashed_nonce,
|
||||
static_cast<uint32_t>(sig.sig.size()),
|
||||
{}};
|
||||
std::copy(sig.sig.begin(), sig.sig.end(), r.sig.data());
|
||||
|
||||
auto progress_tracker = store->get_progress_tracker();
|
||||
if (progress_tracker != nullptr)
|
||||
auto result = progress_tracker->add_signature(
|
||||
r.term,
|
||||
r.last_log_idx,
|
||||
r.from_node,
|
||||
r.signature_size,
|
||||
r.sig,
|
||||
hashed_nonce,
|
||||
node_count());
|
||||
for (auto it = nodes.begin(); it != nodes.end(); ++it)
|
||||
{
|
||||
progress_tracker->add_signature(
|
||||
r.term, r.last_log_idx, r.from_node, r.signature_size, r.sig);
|
||||
auto send_to = it->first;
|
||||
if (send_to != state->my_node_id)
|
||||
{
|
||||
channels->send_authenticated(
|
||||
ccf::NodeMsgType::consensus_msg, send_to, r);
|
||||
}
|
||||
}
|
||||
|
||||
channels->send_authenticated(ccf::NodeMsgType::consensus_msg, to, r);
|
||||
try_send_sig_ack(r.term, r.last_log_idx, result);
|
||||
}
|
||||
|
||||
void recv_append_entries_signed_response(const uint8_t* data, size_t size)
|
||||
|
@ -891,13 +924,178 @@ namespace aft
|
|||
}
|
||||
|
||||
auto progress_tracker = store->get_progress_tracker();
|
||||
if (progress_tracker != nullptr)
|
||||
CCF_ASSERT(progress_tracker != nullptr, "progress_tracker is not set");
|
||||
auto result = progress_tracker->add_signature(
|
||||
r.term,
|
||||
r.last_log_idx,
|
||||
r.from_node,
|
||||
r.signature_size,
|
||||
r.sig,
|
||||
r.hashed_nonce,
|
||||
node_count());
|
||||
try_send_sig_ack(r.term, r.last_log_idx, result);
|
||||
}
|
||||
|
||||
void try_send_sig_ack(
|
||||
kv::Consensus::View view,
|
||||
kv::Consensus::SeqNo seqno,
|
||||
kv::TxHistory::Result r)
|
||||
{
|
||||
switch (r)
|
||||
{
|
||||
progress_tracker->add_signature(
|
||||
r.term, r.last_log_idx, r.from_node, r.signature_size, r.sig);
|
||||
case kv::TxHistory::Result::OK:
|
||||
case kv::TxHistory::Result::FAIL:
|
||||
{
|
||||
break;
|
||||
}
|
||||
case kv::TxHistory::Result::SEND_SIG_RECEIPT_ACK:
|
||||
{
|
||||
SignaturesReceivedAck r = {
|
||||
{bft_signature_received_ack, state->my_node_id}, view, seqno};
|
||||
for (auto it = nodes.begin(); it != nodes.end(); ++it)
|
||||
{
|
||||
auto send_to = it->first;
|
||||
if (send_to != state->my_node_id)
|
||||
{
|
||||
channels->send_authenticated(
|
||||
ccf::NodeMsgType::consensus_msg, send_to, r);
|
||||
}
|
||||
}
|
||||
|
||||
auto progress_tracker = store->get_progress_tracker();
|
||||
CCF_ASSERT(
|
||||
progress_tracker != nullptr, "progress_tracker is not set");
|
||||
auto result = progress_tracker->add_signature_ack(
|
||||
view, seqno, state->my_node_id, node_count());
|
||||
try_send_reply_and_nonce(view, seqno, result);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw ccf::ccf_logic_error(fmt::format("Unknown enum type: {}", r));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void recv_signature_received_ack(const uint8_t* data, size_t size)
|
||||
{
|
||||
SignaturesReceivedAck r;
|
||||
|
||||
try
|
||||
{
|
||||
r = channels->template recv_authenticated<SignaturesReceivedAck>(
|
||||
data, size);
|
||||
}
|
||||
catch (const std::logic_error& err)
|
||||
{
|
||||
LOG_FAIL_FMT("Error in recv_signature_received_ack message");
|
||||
LOG_DEBUG_FMT(
|
||||
"Error in recv_signature_received_ack message: {}", err.what());
|
||||
return;
|
||||
}
|
||||
|
||||
auto node = nodes.find(r.from_node);
|
||||
if (node == nodes.end())
|
||||
{
|
||||
// Ignore if we don't recognise the node.
|
||||
LOG_FAIL_FMT(
|
||||
"Recv signature received ack to {} from {}: unknown node",
|
||||
state->my_node_id,
|
||||
r.from_node);
|
||||
return;
|
||||
}
|
||||
|
||||
auto progress_tracker = store->get_progress_tracker();
|
||||
CCF_ASSERT(progress_tracker != nullptr, "progress_tracker is not set");
|
||||
LOG_TRACE_FMT(
|
||||
"processing recv_signature_received_ack, from:{} view:{}, seqno:{}",
|
||||
r.from_node,
|
||||
r.term,
|
||||
r.idx);
|
||||
auto result = progress_tracker->add_signature_ack(
|
||||
r.term, r.idx, r.from_node, node_count());
|
||||
try_send_reply_and_nonce(r.term, r.idx, result);
|
||||
}
|
||||
|
||||
void try_send_reply_and_nonce(
|
||||
kv::Consensus::View view,
|
||||
kv::Consensus::SeqNo seqno,
|
||||
kv::TxHistory::Result r)
|
||||
{
|
||||
switch (r)
|
||||
{
|
||||
case kv::TxHistory::Result::OK:
|
||||
case kv::TxHistory::Result::FAIL:
|
||||
{
|
||||
break;
|
||||
}
|
||||
case kv::TxHistory::Result::SEND_REPLY_AND_NONCE:
|
||||
{
|
||||
Nonce nonce;
|
||||
auto progress_tracker = store->get_progress_tracker();
|
||||
CCF_ASSERT(
|
||||
progress_tracker != nullptr, "progress_tracker is not set");
|
||||
nonce = progress_tracker->get_my_nonce(view, seqno);
|
||||
NonceRevealMsg r = {
|
||||
{bft_nonce_reveal, state->my_node_id}, view, seqno, nonce};
|
||||
|
||||
for (auto it = nodes.begin(); it != nodes.end(); ++it)
|
||||
{
|
||||
auto send_to = it->first;
|
||||
if (send_to != state->my_node_id)
|
||||
{
|
||||
channels->send_authenticated(
|
||||
ccf::NodeMsgType::consensus_msg, send_to, r);
|
||||
}
|
||||
}
|
||||
progress_tracker->add_nonce_reveal(
|
||||
view, seqno, nonce, state->my_node_id);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw ccf::ccf_logic_error(fmt::format("Unknown enum type: {}", r));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void recv_nonce_reveal(const uint8_t* data, size_t size)
|
||||
{
|
||||
NonceRevealMsg r;
|
||||
|
||||
try
|
||||
{
|
||||
r = channels->template recv_authenticated<NonceRevealMsg>(data, size);
|
||||
}
|
||||
catch (const std::logic_error& err)
|
||||
{
|
||||
LOG_FAIL_FMT("Error in recv_signature_received_ack message");
|
||||
LOG_DEBUG_FMT(
|
||||
"Error in recv_signature_received_ack message: {}", err.what());
|
||||
return;
|
||||
}
|
||||
|
||||
auto node = nodes.find(r.from_node);
|
||||
if (node == nodes.end())
|
||||
{
|
||||
// Ignore if we don't recognise the node.
|
||||
LOG_FAIL_FMT(
|
||||
"Recv nonce reveal to {} from {}: unknown node",
|
||||
state->my_node_id,
|
||||
r.from_node);
|
||||
return;
|
||||
}
|
||||
|
||||
auto progress_tracker = store->get_progress_tracker();
|
||||
CCF_ASSERT(progress_tracker != nullptr, "progress_tracker is not set");
|
||||
LOG_TRACE_FMT(
|
||||
"processing nonce_reveal, from:{} view:{}, seqno:{}",
|
||||
r.from_node,
|
||||
r.term,
|
||||
r.idx);
|
||||
progress_tracker->add_nonce_reveal(r.term, r.idx, r.nonce, r.from_node);
|
||||
}
|
||||
|
||||
void recv_append_entries_response(const uint8_t* data, size_t size)
|
||||
{
|
||||
std::lock_guard<SpinLock> guard(state->lock);
|
||||
|
@ -1461,7 +1659,7 @@ namespace aft
|
|||
|
||||
for (auto node_id : to_remove)
|
||||
{
|
||||
if (replica_state == Leader)
|
||||
if (replica_state == Leader || consensus_type == ConsensusType::BFT)
|
||||
{
|
||||
channels->destroy_channel(node_id);
|
||||
}
|
||||
|
@ -1487,13 +1685,16 @@ namespace aft
|
|||
auto index = state->last_idx + 1;
|
||||
nodes.try_emplace(node_info.first, node_info.second, index, 0);
|
||||
|
||||
if (replica_state == Leader)
|
||||
if (replica_state == Leader || consensus_type == ConsensusType::BFT)
|
||||
{
|
||||
channels->create_channel(
|
||||
node_info.first,
|
||||
node_info.second.hostname,
|
||||
node_info.second.port);
|
||||
}
|
||||
|
||||
if (replica_state == Leader)
|
||||
{
|
||||
send_append_entries(node_info.first, index);
|
||||
}
|
||||
|
||||
|
|
|
@ -126,6 +126,11 @@ namespace aft
|
|||
aft->enable_all_domains();
|
||||
}
|
||||
|
||||
uint32_t node_count() override
|
||||
{
|
||||
return aft->node_count();
|
||||
}
|
||||
|
||||
void open_network() override
|
||||
{
|
||||
is_open = true;
|
||||
|
|
|
@ -21,6 +21,7 @@ namespace aft
|
|||
using Term = int64_t;
|
||||
using NodeId = uint64_t;
|
||||
using Node2NodeMsg = uint64_t;
|
||||
using Nonce = std::array<uint8_t, 32>;
|
||||
|
||||
using ReplyCallback = std::function<bool(
|
||||
void* owner,
|
||||
|
@ -133,6 +134,8 @@ namespace aft
|
|||
raft_request_vote_response,
|
||||
|
||||
bft_request,
|
||||
bft_signature_received_ack,
|
||||
bft_nonce_reveal
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
@ -162,10 +165,24 @@ namespace aft
|
|||
{
|
||||
Term term;
|
||||
Index last_log_idx;
|
||||
Nonce hashed_nonce;
|
||||
uint32_t signature_size;
|
||||
std::array<uint8_t, MBEDTLS_ECDSA_MAX_LEN> sig;
|
||||
};
|
||||
|
||||
struct SignaturesReceivedAck : RaftHeader
|
||||
{
|
||||
Term term;
|
||||
Index idx;
|
||||
};
|
||||
|
||||
struct NonceRevealMsg : RaftHeader
|
||||
{
|
||||
Term term;
|
||||
Index idx;
|
||||
Nonce nonce;
|
||||
};
|
||||
|
||||
struct RequestVote : RaftHeader
|
||||
{
|
||||
Term term;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache 2.0 License.
|
||||
#pragma once
|
||||
|
||||
#include "logger_formatters.h"
|
||||
#include "ring_buffer.h"
|
||||
#include "thread_ids.h"
|
||||
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the Apache 2.0 License.
|
||||
#pragma once
|
||||
|
||||
#define FMT_HEADER_ONLY
|
||||
#include <fmt/format.h>
|
||||
#include <msgpack/msgpack.hpp>
|
||||
#include <sstream>
|
||||
|
||||
namespace fmt
|
||||
{
|
||||
inline std::string uint8_vector_to_hex_string(const std::vector<uint8_t>& v)
|
||||
{
|
||||
std::stringstream ss;
|
||||
for (auto it = v.begin(); it != v.end(); it++)
|
||||
{
|
||||
ss << std::hex << static_cast<unsigned>(*it);
|
||||
}
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
template <>
|
||||
struct formatter<std::vector<uint8_t>>
|
||||
{
|
||||
template <typename ParseContext>
|
||||
constexpr auto parse(ParseContext& ctx)
|
||||
{
|
||||
return ctx.begin();
|
||||
}
|
||||
|
||||
template <typename FormatContext>
|
||||
auto format(const std::vector<uint8_t>& p, FormatContext& ctx)
|
||||
{
|
||||
return format_to(ctx.out(), uint8_vector_to_hex_string(p));
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct formatter<std::array<uint8_t, 32>>
|
||||
{
|
||||
template <typename ParseContext>
|
||||
constexpr auto parse(ParseContext& ctx)
|
||||
{
|
||||
return ctx.begin();
|
||||
}
|
||||
|
||||
template <typename FormatContext>
|
||||
auto format(const std::array<uint8_t, 32>& p, FormatContext& ctx)
|
||||
{
|
||||
return format_to(
|
||||
ctx.out(), uint8_vector_to_hex_string({p.begin(), p.end()}));
|
||||
}
|
||||
};
|
||||
}
|
|
@ -146,15 +146,24 @@ namespace kv
|
|||
std::vector<uint8_t> response;
|
||||
};
|
||||
|
||||
enum class Result
|
||||
{
|
||||
FAIL = 0,
|
||||
OK,
|
||||
SEND_SIG_RECEIPT_ACK,
|
||||
SEND_REPLY_AND_NONCE
|
||||
};
|
||||
|
||||
using ResultCallbackHandler = std::function<bool(ResultCallbackArgs)>;
|
||||
using ResponseCallbackHandler = std::function<bool(ResponseCallbackArgs)>;
|
||||
|
||||
virtual ~TxHistory() {}
|
||||
virtual void append(const std::vector<uint8_t>& replicated) = 0;
|
||||
virtual void append(const uint8_t* replicated, size_t replicated_size) = 0;
|
||||
virtual bool verify_and_sign(
|
||||
virtual Result verify_and_sign(
|
||||
ccf::PrimarySignature& signature, Term* term = nullptr) = 0;
|
||||
virtual bool verify(Term* term = nullptr) = 0;
|
||||
virtual bool verify(
|
||||
Term* term = nullptr, ccf::PrimarySignature* sig = nullptr) = 0;
|
||||
virtual void emit_signature() = 0;
|
||||
virtual crypto::Sha256Hash get_replicated_state_root() = 0;
|
||||
virtual std::vector<uint8_t> get_receipt(Version v) = 0;
|
||||
|
@ -314,6 +323,7 @@ namespace kv
|
|||
}
|
||||
virtual void enable_all_domains() {}
|
||||
|
||||
virtual uint32_t node_count() = 0;
|
||||
virtual void open_network() = 0;
|
||||
virtual void emit_signature() = 0;
|
||||
virtual ConsensusType type() = 0;
|
||||
|
|
|
@ -785,10 +785,16 @@ namespace kv
|
|||
}
|
||||
|
||||
auto h = get_history();
|
||||
bool result;
|
||||
bool result = true;
|
||||
if (sig != nullptr)
|
||||
{
|
||||
result = h->verify_and_sign(*sig, term_);
|
||||
auto r = h->verify_and_sign(*sig, term_);
|
||||
if (
|
||||
r != kv::TxHistory::Result::OK &&
|
||||
r != kv::TxHistory::Result::SEND_SIG_RECEIPT_ACK)
|
||||
{
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -708,7 +708,7 @@ TEST_CASE("Deserialise return status")
|
|||
{
|
||||
kv::Tx tx(store.next_version());
|
||||
auto sig_view = tx.get_view(signatures);
|
||||
ccf::PrimarySignature sigv(0, 2);
|
||||
ccf::PrimarySignature sigv(0, 2, {0});
|
||||
sig_view->put(0, sigv);
|
||||
auto [success, reqid, data] = tx.commit_reserved();
|
||||
REQUIRE(success == kv::CommitSuccess::OK);
|
||||
|
@ -720,7 +720,7 @@ TEST_CASE("Deserialise return status")
|
|||
{
|
||||
kv::Tx tx(store.next_version());
|
||||
auto [sig_view, data_view] = tx.get_view(signatures, data);
|
||||
ccf::PrimarySignature sigv(0, 2);
|
||||
ccf::PrimarySignature sigv(0, 2, {0});
|
||||
sig_view->put(0, sigv);
|
||||
data_view->put(43, 43);
|
||||
auto [success, reqid, data] = tx.commit_reserved();
|
||||
|
|
|
@ -145,6 +145,11 @@ namespace kv
|
|||
return;
|
||||
}
|
||||
|
||||
uint32_t node_count() override
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
void emit_signature() override
|
||||
{
|
||||
return;
|
||||
|
|
|
@ -109,12 +109,12 @@ namespace ccf
|
|||
|
||||
void append(const uint8_t*, size_t) override {}
|
||||
|
||||
bool verify_and_sign(PrimarySignature&, kv::Term*) override
|
||||
kv::TxHistory::Result verify_and_sign(PrimarySignature&, kv::Term*) override
|
||||
{
|
||||
return true;
|
||||
return kv::TxHistory::Result::OK;
|
||||
}
|
||||
|
||||
bool verify(kv::Term*) override
|
||||
bool verify(kv::Term*, ccf::PrimarySignature*) override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
@ -142,7 +142,7 @@ namespace ccf
|
|||
[txid, this]() {
|
||||
kv::Tx sig(txid.version);
|
||||
auto sig_view = sig.get_view(signatures);
|
||||
PrimarySignature sig_value(id, txid.version);
|
||||
PrimarySignature sig_value(id, txid.version, {0});
|
||||
sig_view->put(0, sig_value);
|
||||
return sig.commit_reserved();
|
||||
},
|
||||
|
@ -576,22 +576,34 @@ namespace ccf
|
|||
replicated_state_tree.append(rh);
|
||||
}
|
||||
|
||||
bool verify_and_sign(
|
||||
kv::TxHistory::Result verify_and_sign(
|
||||
PrimarySignature& sig, kv::Term* term = nullptr) override
|
||||
{
|
||||
if (!verify(term))
|
||||
if (!verify(term, &sig))
|
||||
{
|
||||
return false;
|
||||
return kv::TxHistory::Result::FAIL;
|
||||
}
|
||||
|
||||
sig.node = id;
|
||||
crypto::Sha256Hash root = replicated_state_tree.get_root();
|
||||
sig.sig = kp.sign_hash(root.h.data(), root.h.size());
|
||||
kv::TxHistory::Result result = kv::TxHistory::Result::OK;
|
||||
|
||||
return true;
|
||||
auto progress_tracker = store.get_progress_tracker();
|
||||
CCF_ASSERT(progress_tracker != nullptr, "progress_tracker is not set");
|
||||
result = progress_tracker->record_primary(
|
||||
sig.view,
|
||||
sig.seqno,
|
||||
sig.node,
|
||||
sig.root,
|
||||
sig.hashed_nonce,
|
||||
store.get_consensus()->node_count());
|
||||
|
||||
sig.node = id;
|
||||
sig.sig = kp.sign_hash(sig.root.h.data(), sig.root.h.size());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool verify(kv::Term* term = nullptr) override
|
||||
bool verify(
|
||||
kv::Term* term = nullptr, PrimarySignature* signature = nullptr) override
|
||||
{
|
||||
kv::Tx tx;
|
||||
auto [sig_tv, ni_tv] = tx.get_view(signatures, nodes);
|
||||
|
@ -601,12 +613,17 @@ namespace ccf
|
|||
LOG_FAIL_FMT("No signature found in signatures map");
|
||||
return false;
|
||||
}
|
||||
auto sig_value = sig.value();
|
||||
auto& sig_value = sig.value();
|
||||
if (term)
|
||||
{
|
||||
*term = sig_value.view;
|
||||
}
|
||||
|
||||
if (signature)
|
||||
{
|
||||
*signature = sig_value;
|
||||
}
|
||||
|
||||
auto ni = ni_tv->get(sig_value.node);
|
||||
if (!ni.has_value())
|
||||
{
|
||||
|
@ -628,12 +645,6 @@ namespace ccf
|
|||
return false;
|
||||
}
|
||||
|
||||
auto progress_tracker = store.get_progress_tracker();
|
||||
if (progress_tracker)
|
||||
{
|
||||
progress_tracker->record_primary(sig_value.view, sig_value.seqno, root);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -682,10 +693,33 @@ namespace ccf
|
|||
auto sig_view = sig.get_view(signatures);
|
||||
crypto::Sha256Hash root = replicated_state_tree.get_root();
|
||||
|
||||
auto progress_tracker = store.get_progress_tracker();
|
||||
if (progress_tracker)
|
||||
Nonce hashed_nonce;
|
||||
auto consensus = store.get_consensus();
|
||||
if (consensus != nullptr && consensus->type() == ConsensusType::BFT)
|
||||
{
|
||||
progress_tracker->record_primary(txid.term, txid.version, root);
|
||||
auto progress_tracker = store.get_progress_tracker();
|
||||
CCF_ASSERT(
|
||||
progress_tracker != nullptr, "progress_tracker is not set");
|
||||
auto r = progress_tracker->record_primary(
|
||||
txid.term, txid.version, id, root, hashed_nonce);
|
||||
if (r != kv::TxHistory::Result::OK)
|
||||
{
|
||||
throw ccf::ccf_logic_error(fmt::format(
|
||||
"Expected success when primary added signature to the "
|
||||
"progress "
|
||||
"tracker. r:{}, view:{}, seqno:{}",
|
||||
r,
|
||||
txid.term,
|
||||
txid.version));
|
||||
}
|
||||
|
||||
auto h =
|
||||
progress_tracker->get_my_hashed_nonce(txid.term, txid.version);
|
||||
std::copy(h.begin(), h.end(), hashed_nonce.begin());
|
||||
}
|
||||
else
|
||||
{
|
||||
hashed_nonce.fill(0);
|
||||
}
|
||||
|
||||
PrimarySignature sig_value(
|
||||
|
@ -695,6 +729,7 @@ namespace ccf
|
|||
commit_txid.second,
|
||||
commit_txid.first,
|
||||
root,
|
||||
hashed_nonce,
|
||||
kp.sign_hash(root.h.data(), root.h.size()),
|
||||
replicated_state_tree.serialise());
|
||||
|
||||
|
|
|
@ -3,30 +3,39 @@
|
|||
#pragma once
|
||||
#include "ds/json.h"
|
||||
#include "entities.h"
|
||||
#include "tls/hash.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace ccf
|
||||
{
|
||||
using Nonce = std::array<uint8_t, 32>;
|
||||
|
||||
struct NodeSignature
|
||||
{
|
||||
std::vector<uint8_t> sig;
|
||||
ccf::NodeId node;
|
||||
Nonce hashed_nonce;
|
||||
|
||||
NodeSignature(const std::vector<uint8_t>& sig_, NodeId node_) :
|
||||
NodeSignature(
|
||||
const std::vector<uint8_t>& sig_, NodeId node_, Nonce hashed_nonce_) :
|
||||
sig(sig_),
|
||||
node(node_)
|
||||
node(node_),
|
||||
hashed_nonce(hashed_nonce_)
|
||||
{}
|
||||
NodeSignature(ccf::NodeId node_, Nonce hashed_nonce_) :
|
||||
node(node_),
|
||||
hashed_nonce(hashed_nonce_)
|
||||
{}
|
||||
NodeSignature(ccf::NodeId node_) : node(node_) {}
|
||||
NodeSignature() = default;
|
||||
|
||||
bool operator==(const NodeSignature& o) const
|
||||
{
|
||||
return sig == o.sig;
|
||||
return sig == o.sig && hashed_nonce == o.hashed_nonce;
|
||||
}
|
||||
|
||||
MSGPACK_DEFINE(sig, node);
|
||||
MSGPACK_DEFINE(sig, node, hashed_nonce);
|
||||
};
|
||||
DECLARE_JSON_TYPE(NodeSignature);
|
||||
DECLARE_JSON_REQUIRED_FIELDS(NodeSignature, sig, node);
|
||||
DECLARE_JSON_REQUIRED_FIELDS(NodeSignature, sig, node, hashed_nonce);
|
||||
}
|
|
@ -8,6 +8,7 @@
|
|||
#include "kv/tx.h"
|
||||
#include "node_signature.h"
|
||||
#include "nodes.h"
|
||||
#include "tls/hash.h"
|
||||
#include "tls/tls.h"
|
||||
#include "tls/verifier.h"
|
||||
|
||||
|
@ -19,16 +20,22 @@ namespace ccf
|
|||
class ProgressTracker
|
||||
{
|
||||
public:
|
||||
ProgressTracker(kv::NodeId id_, ccf::Nodes& nodes_) : id(id_), nodes(nodes_)
|
||||
ProgressTracker(kv::NodeId id_, ccf::Nodes& nodes_) :
|
||||
id(id_),
|
||||
nodes(nodes_),
|
||||
entropy(tls::create_entropy())
|
||||
{}
|
||||
|
||||
void add_signature(
|
||||
kv::TxHistory::Result add_signature(
|
||||
kv::Consensus::View view,
|
||||
kv::Consensus::SeqNo seqno,
|
||||
kv::NodeId node_id,
|
||||
uint32_t signature_size,
|
||||
std::array<uint8_t, MBEDTLS_ECDSA_MAX_LEN>& sig)
|
||||
std::array<uint8_t, MBEDTLS_ECDSA_MAX_LEN>& sig,
|
||||
Nonce& hashed_nonce,
|
||||
uint32_t node_count)
|
||||
{
|
||||
LOG_TRACE_FMT("add_signature node_id:{}, seqno:{}", node_id, seqno);
|
||||
auto it = certificates.find(CertKey(view, seqno));
|
||||
if (it == certificates.end())
|
||||
{
|
||||
|
@ -53,7 +60,7 @@ namespace ccf
|
|||
node_id,
|
||||
view,
|
||||
seqno));
|
||||
return;
|
||||
return kv::TxHistory::Result::FAIL;
|
||||
}
|
||||
LOG_TRACE_FMT(
|
||||
"Signature verification from {} passed, view:{}, seqno:{}",
|
||||
|
@ -64,7 +71,7 @@ namespace ccf
|
|||
|
||||
std::vector<uint8_t> sig_vec;
|
||||
CCF_ASSERT(
|
||||
signature_size < sig.size(),
|
||||
signature_size <= sig.size(),
|
||||
fmt::format(
|
||||
"Invalid signature size, signature_size:{}, sig.size:{}",
|
||||
signature_size,
|
||||
|
@ -72,27 +79,64 @@ namespace ccf
|
|||
sig_vec.assign(sig.begin(), sig.begin() + signature_size);
|
||||
|
||||
auto& cert = it->second;
|
||||
cert.sigs.insert(std::pair<kv::NodeId, ccf::NodeSignature>(
|
||||
node_id, {std::move(sig_vec), node_id}));
|
||||
BftNodeSignature bft_node_sig(std::move(sig_vec), node_id, hashed_nonce);
|
||||
try_match_unmatched_nonces(cert, bft_node_sig, view, seqno, node_id);
|
||||
cert.sigs.insert(std::pair<kv::NodeId, BftNodeSignature>(
|
||||
node_id, std::move(bft_node_sig)));
|
||||
|
||||
if (can_send_sig_ack(cert, node_count))
|
||||
{
|
||||
return kv::TxHistory::Result::SEND_SIG_RECEIPT_ACK;
|
||||
}
|
||||
return kv::TxHistory::Result::OK;
|
||||
}
|
||||
|
||||
void record_primary(
|
||||
kv::TxHistory::Result record_primary(
|
||||
kv::Consensus::View view,
|
||||
kv::Consensus::SeqNo seqno,
|
||||
crypto::Sha256Hash& root)
|
||||
kv::NodeId node_id,
|
||||
crypto::Sha256Hash& root,
|
||||
Nonce& hashed_nonce,
|
||||
uint32_t node_count = 0)
|
||||
{
|
||||
auto n = entropy->random(hashed_nonce.size());
|
||||
Nonce my_nonce;
|
||||
std::copy(n.begin(), n.end(), my_nonce.begin());
|
||||
if (node_id == id)
|
||||
{
|
||||
auto h = hash_data(my_nonce);
|
||||
std::copy(h.begin(), h.end(), hashed_nonce.begin());
|
||||
}
|
||||
|
||||
auto it = certificates.find(CertKey(view, seqno));
|
||||
if (it == certificates.end())
|
||||
{
|
||||
certificates.insert(std::pair<CertKey, CommitCert>(
|
||||
CertKey(view, seqno), CommitCert(root)));
|
||||
return;
|
||||
CommitCert cert(root, my_nonce);
|
||||
BftNodeSignature bft_node_sig({}, node_id, hashed_nonce);
|
||||
bft_node_sig.is_primary = true;
|
||||
try_match_unmatched_nonces(cert, bft_node_sig, view, seqno, node_id);
|
||||
cert.sigs.insert(
|
||||
std::pair<kv::NodeId, BftNodeSignature>(node_id, bft_node_sig));
|
||||
|
||||
certificates.insert(
|
||||
std::pair<CertKey, CommitCert>(CertKey(view, seqno), cert));
|
||||
|
||||
LOG_TRACE_FMT("Adding new root for view:{}, seqno:{}", view, seqno);
|
||||
return kv::TxHistory::Result::OK;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We received some entries before we got the root so we now need to
|
||||
// verify the signatures
|
||||
auto& cert = it->second;
|
||||
cert.root = root;
|
||||
BftNodeSignature bft_node_sig({}, node_id, hashed_nonce);
|
||||
bft_node_sig.is_primary = true;
|
||||
try_match_unmatched_nonces(cert, bft_node_sig, view, seqno, node_id);
|
||||
cert.sigs.insert(
|
||||
std::pair<kv::NodeId, BftNodeSignature>(node_id, bft_node_sig));
|
||||
cert.my_nonce = my_nonce;
|
||||
cert.have_primary_signature = true;
|
||||
for (auto& sig : cert.sigs)
|
||||
{
|
||||
if (!verify_signature(
|
||||
|
@ -109,15 +153,144 @@ namespace ccf
|
|||
view,
|
||||
seqno));
|
||||
}
|
||||
LOG_TRACE_FMT(
|
||||
"Signature verification from {} passed, view:{}, seqno:{}",
|
||||
sig.second.node,
|
||||
view,
|
||||
seqno);
|
||||
}
|
||||
}
|
||||
|
||||
if (it->second.root != root)
|
||||
auto& cert = it->second;
|
||||
if (cert.root != root)
|
||||
{
|
||||
// NOTE: At this point we have cryptographic proof that someone is being
|
||||
// dishonest we need to work out what to do.
|
||||
throw ccf::ccf_logic_error("We have proof someone is being dishonest");
|
||||
}
|
||||
|
||||
if (node_count > 0 && can_send_sig_ack(cert, node_count))
|
||||
{
|
||||
return kv::TxHistory::Result::SEND_SIG_RECEIPT_ACK;
|
||||
}
|
||||
return kv::TxHistory::Result::OK;
|
||||
}
|
||||
|
||||
kv::TxHistory::Result add_signature_ack(
|
||||
kv::Consensus::View view,
|
||||
kv::Consensus::SeqNo seqno,
|
||||
kv::NodeId node_id,
|
||||
uint32_t node_count = 0)
|
||||
{
|
||||
auto it = certificates.find(CertKey(view, seqno));
|
||||
if (it == certificates.end())
|
||||
{
|
||||
// We currently do not know what the root is, so lets save this
|
||||
// signature and and we will verify the root when we get it from the
|
||||
// primary
|
||||
auto r = certificates.insert(
|
||||
std::pair<CertKey, CommitCert>(CertKey(view, seqno), CommitCert()));
|
||||
it = r.first;
|
||||
}
|
||||
|
||||
LOG_TRACE_FMT(
|
||||
"processing recv_signature_received_ack, from:{} view:{}, seqno:{}",
|
||||
node_id,
|
||||
view,
|
||||
seqno);
|
||||
|
||||
auto& cert = it->second;
|
||||
cert.sig_acks.insert(node_id);
|
||||
|
||||
if (can_send_reply_and_nonce(cert, node_count))
|
||||
{
|
||||
return kv::TxHistory::Result::SEND_REPLY_AND_NONCE;
|
||||
}
|
||||
return kv::TxHistory::Result::OK;
|
||||
}
|
||||
|
||||
void add_nonce_reveal(
|
||||
kv::Consensus::View view,
|
||||
kv::Consensus::SeqNo seqno,
|
||||
Nonce nonce,
|
||||
kv::NodeId node_id)
|
||||
{
|
||||
bool did_add = false;
|
||||
auto it = certificates.find(CertKey(view, seqno));
|
||||
if (it == certificates.end())
|
||||
{
|
||||
// We currently do not know what the root is, so lets save this
|
||||
// signature and and we will verify the root when we get it from the
|
||||
// primary
|
||||
auto r = certificates.insert(
|
||||
std::pair<CertKey, CommitCert>(CertKey(view, seqno), CommitCert()));
|
||||
it = r.first;
|
||||
did_add = true;
|
||||
}
|
||||
|
||||
auto& cert = it->second;
|
||||
auto it_node_sig = cert.sigs.find(node_id);
|
||||
if (it_node_sig == cert.sigs.end())
|
||||
{
|
||||
cert.unmatched_nonces.insert(
|
||||
std::pair<kv::NodeId, Nonce>(node_id, nonce));
|
||||
return;
|
||||
}
|
||||
|
||||
BftNodeSignature& sig = it_node_sig->second;
|
||||
LOG_TRACE_FMT(
|
||||
"add_nonce_reveal view:{}, seqno:{}, node_id:{}, sig.hashed_nonce:{}, "
|
||||
" received.nonce:{}, hash(received.nonce):{} did_add:{}",
|
||||
view,
|
||||
seqno,
|
||||
node_id,
|
||||
sig.hashed_nonce,
|
||||
nonce,
|
||||
hash_data(nonce),
|
||||
did_add);
|
||||
|
||||
if (!match_nonces(hash_data(nonce), sig.hashed_nonce))
|
||||
{
|
||||
// NOTE: We need to handle this case but for now having this make a
|
||||
// test fail will be very handy
|
||||
LOG_FAIL_FMT(
|
||||
"Nonces do not match add_nonce_reveal view:{}, seqno:{}, node_id:{}, "
|
||||
"sig.hashed_nonce:{}, "
|
||||
" received.nonce:{}, hash(received.nonce):{} did_add:{}",
|
||||
view,
|
||||
seqno,
|
||||
node_id,
|
||||
sig.hashed_nonce,
|
||||
nonce,
|
||||
hash_data(nonce),
|
||||
did_add);
|
||||
throw ccf::ccf_logic_error(fmt::format(
|
||||
"nonces do not match verification from {} FAILED, view:{}, seqno:{}",
|
||||
node_id,
|
||||
view,
|
||||
seqno));
|
||||
}
|
||||
sig.nonce = nonce;
|
||||
}
|
||||
|
||||
Nonce get_my_nonce(kv::Consensus::View view, kv::Consensus::SeqNo seqno)
|
||||
{
|
||||
auto it = certificates.find(CertKey(view, seqno));
|
||||
if (it == certificates.end())
|
||||
{
|
||||
throw ccf::ccf_logic_error(fmt::format(
|
||||
"Attempting to access unknown nonce, view:{}, seqno:{}",
|
||||
view,
|
||||
seqno));
|
||||
}
|
||||
return it->second.my_nonce;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> get_my_hashed_nonce(
|
||||
kv::Consensus::View view, kv::Consensus::SeqNo seqno)
|
||||
{
|
||||
Nonce nonce = get_my_nonce(view, seqno);
|
||||
return hash_data(nonce);
|
||||
}
|
||||
|
||||
void set_node_id(kv::NodeId id_)
|
||||
|
@ -128,6 +301,7 @@ namespace ccf
|
|||
private:
|
||||
kv::NodeId id;
|
||||
ccf::Nodes& nodes;
|
||||
std::shared_ptr<tls::Entropy> entropy;
|
||||
|
||||
struct CertKey
|
||||
{
|
||||
|
@ -149,14 +323,36 @@ namespace ccf
|
|||
}
|
||||
};
|
||||
|
||||
struct BftNodeSignature : public ccf::NodeSignature
|
||||
{
|
||||
bool is_primary;
|
||||
Nonce nonce;
|
||||
|
||||
BftNodeSignature(
|
||||
const std::vector<uint8_t>& sig_, NodeId node_, Nonce hashed_nonce_) :
|
||||
NodeSignature(sig_, node_, hashed_nonce_),
|
||||
is_primary(false)
|
||||
{}
|
||||
};
|
||||
|
||||
struct CommitCert
|
||||
{
|
||||
CommitCert(crypto::Sha256Hash& root_) : root(root_) {}
|
||||
CommitCert(crypto::Sha256Hash& root_, Nonce my_nonce_) :
|
||||
root(root_),
|
||||
my_nonce(my_nonce_),
|
||||
have_primary_signature(true)
|
||||
{}
|
||||
|
||||
CommitCert() = default;
|
||||
|
||||
crypto::Sha256Hash root;
|
||||
// std::map<kv::NodeId, std::vector<uint8_t>> sigs;
|
||||
std::map<kv::NodeId, ccf::NodeSignature> sigs;
|
||||
std::map<kv::NodeId, BftNodeSignature> sigs;
|
||||
std::set<kv::NodeId> sig_acks;
|
||||
std::map<kv::NodeId, Nonce> unmatched_nonces;
|
||||
Nonce my_nonce;
|
||||
bool have_primary_signature = false;
|
||||
bool ack_sent = false;
|
||||
bool reply_and_nonce_sent = false;
|
||||
};
|
||||
std::map<CertKey, CommitCert> certificates;
|
||||
|
||||
|
@ -180,5 +376,98 @@ namespace ccf
|
|||
return from_cert->verify_hash(
|
||||
root.h.data(), root.h.size(), sig, sig_size);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> hash_data(Nonce data)
|
||||
{
|
||||
tls::HashBytes hash;
|
||||
tls::do_hash(
|
||||
reinterpret_cast<const uint8_t*>(&data),
|
||||
data.size(),
|
||||
hash,
|
||||
MBEDTLS_MD_SHA256);
|
||||
return hash;
|
||||
}
|
||||
|
||||
void try_match_unmatched_nonces(
|
||||
CommitCert& cert,
|
||||
BftNodeSignature& bft_node_sig,
|
||||
kv::Consensus::View view,
|
||||
kv::Consensus::SeqNo seqno,
|
||||
kv::NodeId node_id)
|
||||
{
|
||||
auto it_unmatched_nonces = cert.unmatched_nonces.find(node_id);
|
||||
if (it_unmatched_nonces != cert.unmatched_nonces.end())
|
||||
{
|
||||
if (!match_nonces(
|
||||
hash_data(it_unmatched_nonces->second),
|
||||
bft_node_sig.hashed_nonce))
|
||||
{
|
||||
// NOTE: We need to handle this case but for now having this make a
|
||||
// test fail will be very handy
|
||||
LOG_FAIL_FMT(
|
||||
"Nonces do not match add_nonce_reveal view:{}, seqno:{}, "
|
||||
"node_id:{}, "
|
||||
"sig.hashed_nonce:{}, "
|
||||
" received.nonce:{}, hash(received.nonce):{}",
|
||||
view,
|
||||
seqno,
|
||||
node_id,
|
||||
bft_node_sig.hashed_nonce,
|
||||
it_unmatched_nonces->second,
|
||||
hash_data(it_unmatched_nonces->second));
|
||||
throw ccf::ccf_logic_error(fmt::format(
|
||||
"nonces do not match verification from {} FAILED, view:{}, "
|
||||
"seqno:{}",
|
||||
node_id,
|
||||
view,
|
||||
seqno));
|
||||
}
|
||||
bft_node_sig.nonce = it_unmatched_nonces->second;
|
||||
cert.unmatched_nonces.erase(it_unmatched_nonces);
|
||||
}
|
||||
}
|
||||
|
||||
bool match_nonces(std::vector<uint8_t> n_1, Nonce n_2)
|
||||
{
|
||||
if (n_1.size() != n_2.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return std::equal(n_1.begin(), n_1.end(), n_2.begin());
|
||||
}
|
||||
|
||||
uint32_t get_message_threshold(uint32_t node_count)
|
||||
{
|
||||
uint32_t f = 0;
|
||||
for (; 3 * f + 1 < node_count; ++f)
|
||||
;
|
||||
|
||||
return 2 * f + 1;
|
||||
}
|
||||
|
||||
bool can_send_sig_ack(CommitCert& cert, uint32_t node_count)
|
||||
{
|
||||
if (
|
||||
cert.sigs.size() >= get_message_threshold(node_count) &&
|
||||
!cert.ack_sent && cert.have_primary_signature)
|
||||
{
|
||||
cert.ack_sent = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool can_send_reply_and_nonce(CommitCert& cert, uint32_t node_count)
|
||||
{
|
||||
if (
|
||||
cert.sig_acks.size() >= get_message_threshold(node_count) &&
|
||||
!cert.reply_and_nonce_sent && cert.ack_sent)
|
||||
{
|
||||
cert.reply_and_nonce_sent = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -31,8 +31,8 @@ namespace ccf
|
|||
|
||||
PrimarySignature() {}
|
||||
|
||||
PrimarySignature(ccf::NodeId node_, ObjectId seqno_) :
|
||||
NodeSignature(node_),
|
||||
PrimarySignature(ccf::NodeId node_, ObjectId seqno_, Nonce hashed_nonce) :
|
||||
NodeSignature(node_, hashed_nonce),
|
||||
seqno(seqno_)
|
||||
{}
|
||||
|
||||
|
@ -45,9 +45,10 @@ namespace ccf
|
|||
ObjectId commit_seqno_,
|
||||
ObjectId commit_view_,
|
||||
const crypto::Sha256Hash root_,
|
||||
Nonce hashed_nonce_,
|
||||
const std::vector<uint8_t>& sig_,
|
||||
const std::vector<uint8_t>& tree_) :
|
||||
NodeSignature(sig_, node_),
|
||||
NodeSignature(sig_, node_, hashed_nonce_),
|
||||
seqno(seqno_),
|
||||
view(view_),
|
||||
commit_seqno(commit_seqno_),
|
||||
|
|
|
@ -61,6 +61,7 @@ TEST_CASE("Check signature verification")
|
|||
{
|
||||
auto encryptor = std::make_shared<kv::NullTxEncryptor>();
|
||||
kv::Store primary_store;
|
||||
|
||||
primary_store.set_encryptor(encryptor);
|
||||
auto& primary_nodes = primary_store.create<ccf::Nodes>(
|
||||
ccf::Tables::NODES, kv::SecurityDomain::PUBLIC);
|
||||
|
@ -113,7 +114,7 @@ TEST_CASE("Check signature verification")
|
|||
{
|
||||
kv::Tx txs;
|
||||
auto tx = txs.get_view(primary_signatures);
|
||||
ccf::PrimarySignature bogus(0, 0);
|
||||
ccf::PrimarySignature bogus(0, 0, {0});
|
||||
bogus.sig = std::vector<uint8_t>(MBEDTLS_ECDSA_MAX_LEN, 1);
|
||||
tx->put(0, bogus);
|
||||
REQUIRE(txs.commit() == kv::CommitSuccess::NO_REPLICATE);
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the Apache 2.0 License.
|
||||
|
||||
#include "node/progress_tracker.h"
|
||||
|
||||
#include "kv/store.h"
|
||||
#include "node/nodes.h"
|
||||
|
||||
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
||||
#include <doctest/doctest.h>
|
||||
#include <string>
|
||||
|
||||
TEST_CASE("Ordered Execution")
|
||||
{
|
||||
kv::Store store;
|
||||
auto& nodes =
|
||||
store.create<ccf::Nodes>(ccf::Tables::NODES, kv::SecurityDomain::PUBLIC);
|
||||
|
||||
kv::Consensus::View view = 0;
|
||||
kv::Consensus::SeqNo seqno = 0;
|
||||
uint32_t node_count = 4;
|
||||
|
||||
crypto::Sha256Hash root;
|
||||
std::array<uint8_t, MBEDTLS_ECDSA_MAX_LEN> sig;
|
||||
ccf::Nonce nonce;
|
||||
|
||||
INFO("Adding signature");
|
||||
{
|
||||
auto pt = std::make_unique<ccf::ProgressTracker>(0, nodes);
|
||||
auto result = pt->add_signature(
|
||||
view, seqno, 1, MBEDTLS_ECDSA_MAX_LEN, sig, nonce, node_count);
|
||||
REQUIRE(result == kv::TxHistory::Result::OK);
|
||||
REQUIRE_THROWS(pt->record_primary(view, seqno, 0, root, nonce, node_count));
|
||||
}
|
||||
|
||||
INFO("Waits for signature tx");
|
||||
{
|
||||
auto pt = std::make_unique<ccf::ProgressTracker>(0, nodes);
|
||||
for (size_t i = 0; i < node_count; ++i)
|
||||
{
|
||||
auto result = pt->add_signature_ack(view, seqno, i, node_count);
|
||||
REQUIRE(result == kv::TxHistory::Result::OK);
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче