diff --git a/unittests/finality_proof.hpp b/unittests/finality_proof.hpp index 535b8f9720..2520acd584 100644 --- a/unittests/finality_proof.hpp +++ b/unittests/finality_proof.hpp @@ -10,33 +10,72 @@ namespace finality_proof { // data relevant to IBC struct ibc_block_data_t { - signed_block_ptr block; + signed_block_ptr block = nullptr; qc_data_t qc_data; action_trace onblock_trace; finality_data_t finality_data; - uint32_t active_finalizer_policy_generation = 0; - uint32_t last_pending_finalizer_policy_generation = 0; - uint32_t last_proposed_finalizer_policy_generation = 0; + finalizer_policy active_finalizer_policy; + finalizer_policy last_pending_finalizer_policy; + finalizer_policy last_proposed_finalizer_policy; digest_type action_mroot; //this is the real action_mroot, as returned from finality_data digest_type base_digest; - digest_type active_finalizer_policy_digest; - digest_type last_pending_finalizer_policy_digest; block_timestamp_type last_pending_finalizer_policy_start_timestamp; - digest_type last_proposed_finalizer_policy_digest; digest_type finality_digest; - digest_type level_3_commitments_digest; - digest_type level_2_commitments_digest; + level_3_commitments_t level_3_commitments; + level_2_commitments_t level_2_commitments; digest_type finality_leaf; digest_type finality_root; block_timestamp_type parent_timestamp; + + digest_type level_3_commitments_digest() const { + return fc::sha256::hash(level_3_commitments); + } + + digest_type level_2_commitments_digest() const { + return fc::sha256::hash(level_2_commitments); + } + + }; + + inline std::string bitset_to_input_string(const boost::dynamic_bitset& bitset) { + static const char* hexchar = "0123456789abcdef"; + + boost::dynamic_bitset bs(bitset); + bs.resize((bs.size() + 7) & ~0x7); + assert(bs.size() % 8 == 0); + + std::string result; + result.resize(bs.size() / 4); + for (size_t i = 0; i < bs.size(); i += 4) { + size_t x = 0; + for (size_t j = 0; j < 4; ++j) + x += bs[i+j] << j; + auto slot = i / 4; + result[slot % 2 ? slot - 1 : slot + 1] = hexchar[x]; // flip the two hex digits for each byte + } + return result; + } + + inline std::string binary_to_hex(const std::string& bin) { + boost::dynamic_bitset bitset(bin.size()); + for (size_t i = 0; i < bin.size(); ++i) { + if (bin[i] == '1') { + bitset.set(bin.size() - 1 - i); + } + } + return bitset_to_input_string(bitset); + } + + inline auto finalizers_string = [](const vote_bitset_t finalizers) { + return bitset_to_input_string(finalizers); }; - static digest_type hash_pair(const digest_type& a, const digest_type& b) { + inline digest_type hash_pair(const digest_type& a, const digest_type& b) { return fc::sha256::hash(std::pair(a, b)); } //generate a proof of inclusion for a node at index from a list of leaves - static std::vector generate_proof_of_inclusion(const std::vector& leaves, const size_t index) { + inline std::vector generate_proof_of_inclusion(const std::vector& leaves, const size_t index) { auto _leaves = leaves; auto _index = index; @@ -70,7 +109,7 @@ namespace finality_proof { } //extract instant finality data from block header extension, as well as qc data from block extension - static qc_data_t extract_qc_data(const signed_block_ptr& b) { + inline qc_data_t extract_qc_data(const signed_block_ptr& b) { assert(b); auto hexts = b->validate_and_extract_header_extensions(); if (auto f_entry = hexts.find(finality_extension::extension_id()); f_entry != hexts.end()) { @@ -87,7 +126,7 @@ namespace finality_proof { return {}; } - static bool has_finalizer_policy_diffs(const signed_block_ptr& block){ + inline bool has_finalizer_policy_diffs(const signed_block_ptr& block){ // extract new finalizer policy finality_extension f_ext = block->extract_header_extension(); @@ -96,7 +135,7 @@ namespace finality_proof { } - static finalizer_policy update_finalizer_policy(const signed_block_ptr block, const finalizer_policy& current_policy){ + inline finalizer_policy update_finalizer_policy(const signed_block_ptr block, const finalizer_policy& current_policy){ // extract new finalizer policy finality_extension f_ext = block->extract_header_extension(); @@ -115,8 +154,7 @@ namespace finality_proof { int32_t blocks_since_proposed = 0; }; - template - class proof_test_cluster : public finality_test_cluster { + class proof_test_cluster : public finality_test_cluster<4> { public: /***** @@ -144,6 +182,8 @@ namespace finality_proof { block_timestamp_type timestamp; block_timestamp_type parent_timestamp; + std::vector vote_propagation = {1,1,1}; + block_timestamp_type prev_last_pending_finalizer_policy_start_timestamp; // counter to (optimistically) track internal policy changes @@ -160,7 +200,7 @@ namespace finality_proof { return {finality_leaves.begin(), finality_leaves.begin() + cutoff + 1}; } - ibc_block_data_t process_result(eosio::testing::produce_block_result_t result){ + ibc_block_data_t process_result(const eosio::testing::produce_block_result_t& result){ signed_block_ptr block = result.block; @@ -216,30 +256,31 @@ namespace finality_proof { blocks_since_proposed_policy[last_proposed_finalizer_policy_digest] = {last_proposed_finalizer_policy, 0}; } } + + //process votes + this->process_finalizer_votes(vote_propagation); //enough to reach quorum threshold - //process votes and collect / compute the IBC-relevant data - this->process_votes(1, this->num_needed_for_quorum); //enough to reach quorum threshold - + // compute the IBC-relevant data finality_data_t finality_data = *this->node0.control->head_finality_data(); digest_type action_mroot = finality_data.action_mroot; digest_type base_digest = finality_data.base_digest; // compute commitments used for proving finality violations - digest_type level_3_commitments_digest = fc::sha256::hash(level_3_commitments_t{ + level_3_commitments_t lvl3_commitments { .reversible_blocks_mroot = finality_data.reversible_blocks_mroot, .latest_qc_claim_block_num = finality_data.latest_qc_claim_block_num, .latest_qc_claim_finality_digest = finality_data.latest_qc_claim_finality_digest, .latest_qc_claim_timestamp = finality_data.latest_qc_claim_timestamp, .timestamp = timestamp, .base_digest = base_digest - }); + }; // compute commitments used for proving finalizer policy changes - digest_type level_2_commitments_digest = fc::sha256::hash(level_2_commitments_t{ + level_2_commitments_t lvl2_commitments { .last_pending_fin_pol_digest = last_pending_finalizer_policy_digest, .last_pending_fin_pol_start_timestamp = last_pending_finalizer_policy_start_timestamp, - .l3_commitments_digest = level_3_commitments_digest - }); + .l3_commitments_digest = fc::sha256::hash(lvl3_commitments) + }; // during IF transition, finality_root is always set to an empty digest digest_type finality_root = digest_type(); @@ -252,7 +293,7 @@ namespace finality_proof { .active_finalizer_policy_generation = is_genesis ? 1 : active_finalizer_policy.generation, .last_pending_finalizer_policy_generation = is_genesis ? 1 : last_pending_finalizer_policy.generation, .finality_tree_digest = finality_root, - .l2_commitments_digest = level_2_commitments_digest + .l2_commitments_digest = fc::sha256::hash(lvl2_commitments) }); // compute finality leaf @@ -278,18 +319,15 @@ namespace finality_proof { .qc_data = qc_data, .onblock_trace = onblock_trace, .finality_data = finality_data, - .active_finalizer_policy_generation = active_finalizer_policy.generation, - .last_pending_finalizer_policy_generation = last_pending_finalizer_policy.generation, - .last_proposed_finalizer_policy_generation = last_proposed_finalizer_policy.generation, + .active_finalizer_policy = active_finalizer_policy, + .last_pending_finalizer_policy = last_pending_finalizer_policy, + .last_proposed_finalizer_policy = last_proposed_finalizer_policy, .action_mroot = action_mroot, .base_digest = base_digest, - .active_finalizer_policy_digest = active_finalizer_policy_digest, - .last_pending_finalizer_policy_digest = last_pending_finalizer_policy_digest, .last_pending_finalizer_policy_start_timestamp = last_pending_finalizer_policy_start_timestamp, - .last_proposed_finalizer_policy_digest = last_proposed_finalizer_policy_digest, .finality_digest = finality_digest, - .level_3_commitments_digest = level_3_commitments_digest, - .level_2_commitments_digest = level_2_commitments_digest, + .level_3_commitments = lvl3_commitments, + .level_2_commitments = lvl2_commitments, .finality_leaf = finality_leaf, .finality_root = finality_root , .parent_timestamp = parent_timestamp @@ -312,7 +350,7 @@ namespace finality_proof { } proof_test_cluster(finality_cluster_config_t config = {.transition_to_savanna = false}) - : finality_test_cluster(config) { + : finality_test_cluster<4>(config) { } @@ -321,4 +359,4 @@ namespace finality_proof { }; -} +} \ No newline at end of file diff --git a/unittests/svnn_finality_violation_tests.cpp b/unittests/svnn_finality_violation_tests.cpp new file mode 100644 index 0000000000..da95e44423 --- /dev/null +++ b/unittests/svnn_finality_violation_tests.cpp @@ -0,0 +1,838 @@ +#include +#include + +#include + +#include + +#include +#include +#include "fork_test_utilities.hpp" + +#include + +#include "finality_proof.hpp" + +using namespace eosio::chain; +using namespace eosio::testing; + +using namespace finality_proof; + +using mvo = mutable_variant_object; + +struct finality_block_data { + + uint32_t block_num{0}; + block_timestamp_type timestamp{}; + block_timestamp_type parent_timestamp{}; + digest_type finality_digest{}; + block_timestamp_type last_pending_finalizer_policy_start_timestamp{}; + level_3_commitments_t level_3_commitments{}; + digest_type base_digest{}; + digest_type finality_root{}; + qc_data_t qc_data{}; + +}; + +finality_block_data get_finality_block_data(const ibc_block_data_t& block_result){ + + return finality_block_data { + .block_num = block_result.block->block_num(), + .timestamp = block_result.block->timestamp, + .parent_timestamp = block_result.parent_timestamp, + .finality_digest = block_result.finality_digest, + .last_pending_finalizer_policy_start_timestamp = block_result.last_pending_finalizer_policy_start_timestamp, + .level_3_commitments = block_result.level_3_commitments, + .base_digest = block_result.base_digest, + .finality_root = block_result.finality_root, + .qc_data = block_result.qc_data + }; +} + + +void shouldPass(const finality_proof::proof_test_cluster& chain, const account_name& rule, const mvo& proof){ + + action_trace trace = chain.node0.push_action("violation"_n, rule, "violation"_n, proof)->action_traces[0]; + + std::pair blame = fc::raw::unpack>(trace.return_value); + + //finalizers 0 and 1 are guilty, while finalizer 2 and 3 are innocent, see bitset tests in svnn_ibc_tests + BOOST_TEST(blame.first == "03"); //0011 + BOOST_TEST(blame.second == "0c"); //1100 + +} + +bool shouldFail(const finality_proof::proof_test_cluster& chain, const account_name& rule, const mvo& proof){ + + bool last_action_failed = false; + try { + chain.node0.push_action("violation"_n, rule, "violation"_n, proof); + } + catch (const eosio_assert_message_exception& e){ + last_action_failed = true; + } + + return last_action_failed; + +} + +digest_type compute_block_ref_digest(const ibc_block_data_t& b){ + + block_ref_digest_data data = { + .block_num = b.block->block_num(), + .timestamp = b.block->timestamp, + .finality_digest = b.finality_digest, + .parent_timestamp = b.parent_timestamp + }; + + digest_type digest = fc::sha256::hash(data); + + return digest; + +} + +digest_type compute_block_ref_digest(const finality_block_data& b){ + + block_ref_digest_data data = { + .block_num = b.block_num, + .timestamp = b.timestamp, + .finality_digest = b.finality_digest, + .parent_timestamp = b.parent_timestamp + }; + + digest_type digest = fc::sha256::hash(data); + + return digest; + +} + +std::vector get_reversible_blocks_digests(const std::vector& blocks){ + + std::vector block_ref_digests; + + for (int i = 0 ; i < blocks.size(); i++){ + + finality_block_data b = blocks[i]; + + digest_type digest = compute_block_ref_digest(b); + + block_ref_digests.push_back(digest); + + } + + return block_ref_digests; + +} + +digest_type calculate_reversible_blocks_merkle(const std::vector& blocks){ + + std::vector block_ref_digests = get_reversible_blocks_digests(blocks); + + return calculate_merkle(block_ref_digests); + +} + + +std::pair get_target_reversible_block(const std::vector& reversible_blocks, const uint32_t& block_num){ + + auto itr = std::find_if(reversible_blocks.begin(), reversible_blocks.end(), [&](const finality_block_data& b){ + return b.block_num == block_num; + }); + + size_t index = std::distance(reversible_blocks.begin(), itr); + + return std::make_pair(*itr, index); + +} + +BOOST_AUTO_TEST_SUITE(svnn_finality_violation) + + mvo prepare_rule_1_proof( const finalizer_policy& active_finalizer_policy, + const finality_block_data& fake_qc_block, + const qc_t& fake_qc, + const finality_block_data& real_qc_block, + const qc_t& real_qc){ + + return mvo() + ("finalizer_policy", active_finalizer_policy) + ("proof_1", mvo() + ("qc_block", mvo() + ("major_version", 1) + ("minor_version", 0) + ("active_finalizer_policy_generation", 1) + ("pending_finalizer_policy_generation", 1) + ("last_pending_finalizer_policy_start_timestamp", fake_qc_block.last_pending_finalizer_policy_start_timestamp) + ("last_pending_finalizer_policy", active_finalizer_policy) + ("level_3_commitments", fake_qc_block.level_3_commitments) + ("witness_hash", fake_qc_block.base_digest) + ("finality_mroot", fake_qc_block.finality_root) + ) + ("active_policy_qc", mvo() + ("signature", fake_qc.active_policy_sig.sig.to_string()) + ("strong_votes", finality_proof::finalizers_string(fake_qc.active_policy_sig.strong_votes.value())) + ) + ) + ("proof_2", mvo() + ("qc_block", mvo() + ("major_version", 1) + ("minor_version", 0) + ("active_finalizer_policy_generation", 1) + ("pending_finalizer_policy_generation", 1) + ("last_pending_finalizer_policy_start_timestamp", real_qc_block.last_pending_finalizer_policy_start_timestamp) + ("last_pending_finalizer_policy", active_finalizer_policy) + ("level_3_commitments", real_qc_block.level_3_commitments) + ("witness_hash", real_qc_block.base_digest) + ("finality_mroot", real_qc_block.finality_root) + ) + ("active_policy_qc", mvo() + ("signature", real_qc.active_policy_sig.sig.to_string()) + ("strong_votes", finality_proof::finalizers_string(real_qc.active_policy_sig.strong_votes.value())) + ) + ); + + } + + mvo prepare_rule_2_3_proof( const finalizer_policy& active_finalizer_policy, + const finality_block_data& high_qc_block, + const qc_t& high_qc, + const finality_block_data& low_qc_block, + const qc_t& low_qc, + const finality_block_data& target_reversible_block, + const size_t target_reversible_block_index, + const std::vector& reversible_blocks){ + + std::vector merkle_branches = finality_proof::generate_proof_of_inclusion(get_reversible_blocks_digests(reversible_blocks), target_reversible_block_index); + + return mvo() + ("finalizer_policy", active_finalizer_policy) + ("high_proof", mvo() + ("qc_block", mvo() + ("major_version", 1) + ("minor_version", 0) + ("active_finalizer_policy_generation", 1) + ("pending_finalizer_policy_generation", 1) + ("last_pending_finalizer_policy_start_timestamp", high_qc_block.last_pending_finalizer_policy_start_timestamp) + ("last_pending_finalizer_policy", active_finalizer_policy) + ("level_3_commitments", high_qc_block.level_3_commitments) + ("witness_hash", high_qc_block.base_digest) + ("finality_mroot", high_qc_block.finality_root) + ) + ("active_policy_qc", mvo() + ("signature", high_qc.active_policy_sig.sig.to_string()) + ("strong_votes", finality_proof::finalizers_string(high_qc.active_policy_sig.strong_votes.value())) + ) + ) + ("low_proof", mvo() + ("qc_block", mvo() + ("major_version", 1) + ("minor_version", 0) + ("active_finalizer_policy_generation", 1) + ("pending_finalizer_policy_generation", 1) + ("last_pending_finalizer_policy_start_timestamp", low_qc_block.last_pending_finalizer_policy_start_timestamp) + ("last_pending_finalizer_policy", active_finalizer_policy) + ("level_3_commitments", low_qc_block.level_3_commitments) + ("witness_hash", low_qc_block.base_digest) + ("finality_mroot", low_qc_block.finality_root) + ) + ("active_policy_qc", mvo() + ("signature", low_qc.active_policy_sig.sig.to_string()) + ("strong_votes", finality_proof::finalizers_string(low_qc.active_policy_sig.strong_votes.value())) + ) + ) + ("reversible_proof_of_inclusion", mvo() + ("target_reversible_block_index", target_reversible_block_index) + ("final_reversible_block_index", reversible_blocks.size() - 1) + ("target", mvo() + ("block_num", target_reversible_block.block_num) + ("timestamp", target_reversible_block.timestamp) + ("finality_digest", target_reversible_block.finality_digest) + ("parent_timestamp", target_reversible_block.parent_timestamp) + ) + ("merkle_branches", merkle_branches) + ); + + } + + BOOST_AUTO_TEST_CASE(cluster_vote_propagation_tests) { try { + + finality_proof::proof_test_cluster cluster_1; + finality_proof::proof_test_cluster cluster_2; + finality_proof::proof_test_cluster cluster_3; + + cluster_1.vote_propagation = {1,1,1}; //all finalizers present + cluster_2.vote_propagation = {1,1,0}; //one finalizer missing + cluster_3.vote_propagation = {1,0,0}; //2 finalizers missing (insufficient for finality progress) + + auto genesis_1_block_result = cluster_1.produce_block(); + auto block_1_1_result = cluster_1.produce_block(); + auto block_2_1_result = cluster_1.produce_block(); + auto block_3_1_result = cluster_1.produce_block(); + auto block_4_1_result = cluster_1.produce_block(); + + BOOST_TEST(block_4_1_result.qc_data.qc.has_value()); + + auto genesis_2_block_result = cluster_2.produce_block(); + auto block_1_2_result = cluster_2.produce_block(); + auto block_2_2_result = cluster_2.produce_block(); + auto block_3_2_result = cluster_2.produce_block(); + auto block_4_2_result = cluster_2.produce_block(); + + BOOST_TEST(block_4_2_result.qc_data.qc.has_value()); + + auto genesis_3_block_result = cluster_3.produce_block(); + auto block_1_3_result = cluster_3.produce_block(); + auto block_2_3_result = cluster_3.produce_block(); + auto block_3_3_result = cluster_3.produce_block(); + auto block_4_3_result = cluster_3.produce_block(); + + BOOST_TEST(!block_4_3_result.qc_data.qc.has_value()); + + } FC_LOG_AND_RETHROW() } + + BOOST_AUTO_TEST_CASE(finality_violation_tests) { try { + + //prepare a few actions we will be using for this test + mutable_variant_object create_action = mvo() + ( "issuer", "eosio"_n) + ( "maximum_supply", "100.0000 EOS"); + + mutable_variant_object issue_action = mvo() + ( "to", "eosio"_n) + ( "quantity", "100.0000 EOS") + ( "memo", ""); + + mutable_variant_object initial_transfer = mvo() + ("from", "eosio"_n) + ("to", "user1"_n) + ("quantity", "100.0000 EOS") + ("memo", ""); + + mutable_variant_object user1_transfer = mvo() + ("from", "user1"_n) + ("to", "user2"_n) + ("quantity", "1.0000 EOS") + ("memo", ""); + + //setup a light client data cache + struct data_cache { + + //The data_cache stores data relevant to finality violation proofs. + //It only operates in optimistic mode, which is sufficient for finality violation proofs testing purposes + + //store the last block over which we have a QC, as well as said QC + finality_block_data high_qc_block; + qc_t high_qc; + + //store the last final block, as wall as the first QC over it. The last_final_qc and high_qc together constitute the 2-chains required for finality progress + finality_block_data last_final_block; + qc_t last_final_qc; + + //current active finalizer policy + finalizer_policy active_finalizer_policy; + + //observe a stream of blocks as they are received, and store minimal data required to construct finality violation proofs in the future + ibc_block_data_t scan_block(const ibc_block_data_t& block_result){ + + finality_block_data block_data{ + .block_num = block_result.block->block_num(), + .timestamp = block_result.block->timestamp, + .parent_timestamp = block_result.parent_timestamp, + .finality_digest = block_result.finality_digest, + .last_pending_finalizer_policy_start_timestamp = block_result.last_pending_finalizer_policy_start_timestamp, + .level_3_commitments = block_result.level_3_commitments, + .base_digest = block_result.base_digest, + .finality_root = block_result.finality_root, + .qc_data = block_result.qc_data + }; + + if (block_result.qc_data.qc.has_value()) { + + auto high_qc_block_itr = std::lower_bound( + reversible_blocks.begin(), + reversible_blocks.end(), + block_result.qc_data.qc.value().to_qc_claim().block_num, + [](const auto& r, const auto& block_num) { + return r.block_num < block_num; + } + ); + + if ( high_qc_block_itr != reversible_blocks.end() + && high_qc_block_itr->qc_data.qc.has_value()) { + + auto last_final_block_itr = std::lower_bound( + reversible_blocks.begin(), + reversible_blocks.end(), + high_qc_block_itr->qc_data.qc.value().to_qc_claim().block_num, + [](const auto& r, const auto& block_num) { + return r.block_num < block_num; + } + ); + + if ( last_final_block_itr != reversible_blocks.end() + && block_result.qc_data.qc.value().to_qc_claim().is_strong_qc) { + + //new strong QC + last_final_block = *last_final_block_itr; + last_final_qc = high_qc_block_itr->qc_data.qc.value(); + + reversible_blocks.erase(reversible_blocks.begin(), ++last_final_block_itr); + + } + + high_qc_block = *high_qc_block_itr; + high_qc = block_result.qc_data.qc.value(); + + } + + } + + reversible_blocks.push_back(block_data); + + active_finalizer_policy = block_result.active_finalizer_policy; + + return block_result; + + } + + std::vector get_reversible_blocks(){ + std::vector result(reversible_blocks.begin(), reversible_blocks.end() - 1); + return result; + } + + finality_block_data get_current_block(){ + return reversible_blocks.back(); + } + + private: + + //store the reversible blocks + std::vector reversible_blocks; + + }; + + data_cache light_client_data; + + //setup a "fake" chain and a "real" chain + finality_proof::proof_test_cluster fake_chain; + finality_proof::proof_test_cluster real_chain; + + //initial finalizer policy A + auto policy_indices = fake_chain.fin_policy_indices_0; // start from original set of indices + + //byzantine finalizers 0 and 1 are colluding and partition the network so that honest finalizers 2 and 3 are separated + fake_chain.vote_propagation = {1,1,0}; + real_chain.vote_propagation = {1,0,1}; + + fake_chain.node0.create_accounts( { "user1"_n, "user2"_n, "violation"_n, "eosio.token"_n } ); + real_chain.node0.create_accounts( { "user1"_n, "user2"_n, "violation"_n, "eosio.token"_n } ); + + fake_chain.node0.set_code( "eosio.token"_n, test_contracts::eosio_token_wasm() ); + fake_chain.node0.set_abi( "eosio.token"_n, test_contracts::eosio_token_abi() ); + fake_chain.node0.set_code( "violation"_n, test_contracts::finality_violation_wasm() ); + fake_chain.node0.set_abi( "violation"_n, test_contracts::finality_violation_abi() ); + + real_chain.node0.set_code( "eosio.token"_n, test_contracts::eosio_token_wasm() ); + real_chain.node0.set_abi( "eosio.token"_n, test_contracts::eosio_token_abi() ); + real_chain.node0.set_code( "violation"_n, test_contracts::finality_violation_wasm() ); + real_chain.node0.set_abi( "violation"_n, test_contracts::finality_violation_abi() ); + + fake_chain.node0.push_action("eosio.token"_n, "create"_n, "eosio.token"_n, create_action); + fake_chain.node0.push_action("eosio.token"_n, "issue"_n, "eosio"_n, issue_action); + fake_chain.node0.push_action("eosio.token"_n, "transfer"_n, "eosio"_n, initial_transfer); + + real_chain.node0.push_action("eosio.token"_n, "create"_n, "eosio.token"_n, create_action); + real_chain.node0.push_action("eosio.token"_n, "issue"_n, "eosio"_n, issue_action); + real_chain.node0.push_action("eosio.token"_n, "transfer"_n, "eosio"_n, initial_transfer); + + //produce a few blocks on the fake chain + auto fake_chain_genesis_block_result = light_client_data.scan_block(fake_chain.produce_block()) ; + + auto fake_chain_block_1_result = light_client_data.scan_block(fake_chain.produce_block()); + auto fake_chain_block_2_result = light_client_data.scan_block(fake_chain.produce_block()); + auto fake_chain_block_3_result = light_client_data.scan_block(fake_chain.produce_block()); + auto fake_chain_block_4_result = light_client_data.scan_block(fake_chain.produce_block()); + + BOOST_REQUIRE(fake_chain_block_4_result.qc_data.qc.has_value()); + + BOOST_TEST(light_client_data.get_reversible_blocks().size() == 1u); + + //produce a few blocks on the real chain + auto real_chain_genesis_block_result = real_chain.produce_block(); + auto real_chain_block_1_result = real_chain.produce_block(); + auto real_chain_block_2_result = real_chain.produce_block(); + auto real_chain_block_3_result = real_chain.produce_block(); + auto real_chain_block_4_result = real_chain.produce_block(); + + BOOST_REQUIRE(real_chain_block_4_result.qc_data.qc.has_value()); + + //verify the two chains are the same so far + BOOST_TEST(fake_chain_block_4_result.finality_digest == real_chain_block_4_result.finality_digest); + + //at this point, we can prepare some "invalid proofs" that the contract will reject + mutable_variant_object invalid_rule_1_proof_1 = prepare_rule_1_proof( light_client_data.active_finalizer_policy, + light_client_data.high_qc_block, + light_client_data.high_qc, + get_finality_block_data(real_chain_block_3_result), + get_finality_block_data(real_chain_block_4_result).qc_data.qc.value()); //same finality digest, not a violation proof + + BOOST_CHECK(shouldFail(real_chain, "rule1"_n, invalid_rule_1_proof_1)); + + mutable_variant_object invalid_rule_1_proof_2 = prepare_rule_1_proof( light_client_data.active_finalizer_policy, + light_client_data.high_qc_block, + light_client_data.high_qc, + get_finality_block_data(real_chain_block_2_result), + get_finality_block_data(real_chain_block_3_result).qc_data.qc.value()); //different timestamps, not a violation proof + + BOOST_CHECK(shouldFail(real_chain, "rule1"_n, invalid_rule_1_proof_2)); + + //create a fork by pushing a transaction on the fake chain + fake_chain.node0.push_action("eosio.token"_n, "transfer"_n, "user1"_n, user1_transfer); + + auto fake_chain_block_5_result = light_client_data.scan_block(fake_chain.produce_block()); + auto real_chain_block_5_result = real_chain.produce_block(); + + BOOST_TEST(light_client_data.get_reversible_blocks().size() == 1u); + + //verify the chains have diverged + BOOST_TEST(fake_chain_block_5_result.finality_digest != real_chain_block_5_result.finality_digest); + + //qc over block #5. Policy A is now locked on #5 + auto fake_chain_block_6_result = light_client_data.scan_block(fake_chain.produce_block()); + auto real_chain_block_6_result = real_chain.produce_block(); + + //qc over block #6 makes block #5 final. Since these blocks are different, this is a finality violation. + auto fake_chain_block_7_result = light_client_data.scan_block(fake_chain.produce_block()); + auto real_chain_block_7_result = real_chain.produce_block(); + + auto fake_chain_block_8_result = light_client_data.scan_block(fake_chain.produce_block()); + auto real_chain_block_8_result = real_chain.produce_block(); + + //prove finality violation (violation of rule #1 : Don't vote on different blocks with the same timestamp) + + //block #8 on the real chain contains a strong QC over a different block #7 than what the light client recorded from the fake chain + + //this is a rule #1 finality violation proof + + //it can be proven by verifying that a strong QC on a block of a given timestamp conflicts with another strong QC on a different block with the same timestamp + mutable_variant_object valid_rule_1_proof = prepare_rule_1_proof( light_client_data.active_finalizer_policy, + light_client_data.high_qc_block, + light_client_data.high_qc, + get_finality_block_data(real_chain_block_7_result), + get_finality_block_data(real_chain_block_8_result).qc_data.qc.value()); + + shouldPass(real_chain, "rule1"_n, valid_rule_1_proof); + + //we temporarily disable a finalizer on the fake chain, which allow us to set up a proof of violation of rule #2 + fake_chain.vote_propagation = {1,0,0}; + + //produce a block on a fake chain without propagating votes to all nodes + auto fake_chain_block_9_result = light_client_data.scan_block(fake_chain.produce_block()); + auto real_chain_block_9_result = real_chain.produce_block(); + + BOOST_TEST(light_client_data.get_reversible_blocks().size() == 1u); + + //restore vote propagation for fake chain. This leaves a one-block gap where no finality progress was achieved + fake_chain.vote_propagation = {1,1,0}; + + auto fake_chain_block_10_result = light_client_data.scan_block(fake_chain.produce_block()); + auto real_chain_block_10_result = real_chain.produce_block(); + + //get the reversible blocks for block #10 (should be two digests, block ref data of #8 and #9) + std::vector block_10_reversible_blocks = light_client_data.get_reversible_blocks(); + + BOOST_TEST(light_client_data.get_reversible_blocks().size() == 2u); + BOOST_TEST(block_10_reversible_blocks.size() == 2u); + + BOOST_TEST(compute_block_ref_digest(block_10_reversible_blocks[0]) == compute_block_ref_digest(fake_chain_block_8_result)); + BOOST_TEST(compute_block_ref_digest(block_10_reversible_blocks[1]) == compute_block_ref_digest(fake_chain_block_9_result)); + + //calculate the merkle root of the reversible blocks digests + digest_type block_10_reversible_blocks_merkle_root = calculate_reversible_blocks_merkle(block_10_reversible_blocks); + + //verify the merkle root of the reversible blocks digests is the same as the one recorded by the light client + BOOST_TEST(fake_chain_block_10_result.level_3_commitments.reversible_blocks_mroot == block_10_reversible_blocks_merkle_root); + + auto valid_proof_1_target_reversible_block = get_target_reversible_block(light_client_data.get_reversible_blocks(), real_chain_block_9_result.block->block_num()); + + //Real chain has a QC on #9 carried by #10, but fake chain doesn't + BOOST_TEST(!fake_chain_block_10_result.qc_data.qc.has_value()); + BOOST_TEST(real_chain_block_10_result.qc_data.qc.has_value()); + + auto fake_chain_block_11_result = light_client_data.scan_block(fake_chain.produce_block()); + auto real_chain_block_11_result = real_chain.produce_block(); + + //Things are back to normal, and we have a QC on both chains + BOOST_TEST(fake_chain_block_11_result.qc_data.qc.has_value()); + BOOST_TEST(real_chain_block_11_result.qc_data.qc.has_value()); + + BOOST_TEST(light_client_data.get_reversible_blocks().size() == 3u); + + //Light client recorded the last QC (over block #10) on the fake chain, which was delivered via block #11. + //Block #10 claims a QC over block #8. We provide fake block #10 and the QC over it, as well as the digests of the reversible blocks the light client recorded (block #8 and block #9). + //We also provide the real block #9 and a QC over it, delivered via block #10. + //Since there is a time range conflict, and since the low proof block is not a descendant of the high proof block, this is a proof of violation of rule #2. + mutable_variant_object valid_rule_2_proof_1 = prepare_rule_2_3_proof( light_client_data.active_finalizer_policy, + light_client_data.get_reversible_blocks()[light_client_data.get_reversible_blocks().size()-1], //fake block #10 + light_client_data.get_current_block().qc_data.qc.value(), //QC over fake block #10 + get_finality_block_data(real_chain_block_9_result), + get_finality_block_data(real_chain_block_10_result).qc_data.qc.value(), + valid_proof_1_target_reversible_block.first, + valid_proof_1_target_reversible_block.second, + block_10_reversible_blocks); + + + //The contract correctly accepts the valid proof, as it is a proof of violation of rule #2 + shouldPass(real_chain, "rule2"_n, valid_rule_2_proof_1); + + //Now, to ensure the smart contract rejects invalid proofs, we can test providing a proof from the real chain where the high proof block is a descendant of the low proof block + mutable_variant_object invalid_rule_2_proof_1 = prepare_rule_2_3_proof( light_client_data.active_finalizer_policy, + get_finality_block_data(real_chain_block_10_result), //real block #10 + get_finality_block_data(real_chain_block_11_result).qc_data.qc.value(), //QC over real block #11 + get_finality_block_data(real_chain_block_9_result), + get_finality_block_data(real_chain_block_10_result).qc_data.qc.value(), + get_finality_block_data(real_chain_block_9_result), + 0, + {get_finality_block_data(real_chain_block_9_result)}); + + //The contract rejects the invalid proof + shouldFail(real_chain, "rule2"_n, invalid_rule_2_proof_1); + + //Now, to ensure the smart contract rejects invalid proofs, we can test providing a proof from the real chain where the high proof block is a descendant of the + //low proof block + mutable_variant_object invalid_rule_2_proof_2 = prepare_rule_2_3_proof( light_client_data.active_finalizer_policy, + get_finality_block_data(real_chain_block_10_result), //real block #10 + get_finality_block_data(real_chain_block_11_result).qc_data.qc.value(), //QC over real block #11 + get_finality_block_data(real_chain_block_9_result), + get_finality_block_data(real_chain_block_10_result).qc_data.qc.value(), + valid_proof_1_target_reversible_block.first, + valid_proof_1_target_reversible_block.second, + block_10_reversible_blocks); + + //The contract rejects the invalid proof + shouldFail(real_chain, "rule2"_n, invalid_rule_2_proof_2); + + auto fake_chain_block_12_result = light_client_data.scan_block(fake_chain.produce_block()); + auto real_chain_block_12_result = real_chain.produce_block(); + + BOOST_TEST(fake_chain_block_12_result.qc_data.qc.has_value()); + BOOST_TEST(real_chain_block_12_result.qc_data.qc.has_value()); + + BOOST_TEST(light_client_data.get_reversible_blocks().size() == 1u); + + //we can now test the other possibility for rule #2, where the high proof is actually from the real chain + + //produce a block on the real chain without propagating votes to all nodes + real_chain.vote_propagation = {1,0,0}; + + auto fake_chain_block_13_result = light_client_data.scan_block(fake_chain.produce_block()); + auto real_chain_block_13_result = real_chain.produce_block(); + + BOOST_TEST(light_client_data.get_reversible_blocks().size() == 1u); + + //restore vote propagation on the real chain + real_chain.vote_propagation = {1,0,1}; + + auto fake_chain_block_14_result = light_client_data.scan_block(fake_chain.produce_block()); + auto real_chain_block_14_result = real_chain.produce_block(); + + BOOST_TEST(light_client_data.get_reversible_blocks().size() == 1u); + + //compute reversible digests on block #12 and #13 on the real chain + std::vector block_14_reversible_blocks = { get_finality_block_data(real_chain_block_12_result), + get_finality_block_data(real_chain_block_13_result)}; + + //calculate the merkle root of the reversible blocks digests + digest_type block_14_reversible_blocks_merkle_root = calculate_reversible_blocks_merkle(block_14_reversible_blocks); + + //verify the merkle root of the reversible blocks digests is the same as the one recorded by the light client + BOOST_TEST(real_chain_block_14_result.level_3_commitments.reversible_blocks_mroot == block_14_reversible_blocks_merkle_root); + + //Fake chain has a QC on #13 carried by #14, but real chain doesn't + BOOST_TEST(fake_chain_block_14_result.qc_data.qc.has_value()); + BOOST_TEST(!real_chain_block_14_result.qc_data.qc.has_value()); + + //fake chain stops producing, but real chain continues + auto real_chain_block_15_result = real_chain.produce_block(); + + //Things are back to normal on the real chain, and we have a QC + BOOST_TEST(real_chain_block_15_result.qc_data.qc.has_value()); + + auto valid_proof_2_target_reversible_block = get_target_reversible_block(block_14_reversible_blocks, block_14_reversible_blocks[block_14_reversible_blocks.size()-1].block_num); + + //We discovered a QC (over block #14) on the real chain, which was delivered via block #15. + //Last QC recorded on the fake chain was over block #13, and was delivered by block #14 + //We provide the real block #14 and its QC, the reversible digests at that time (block #12 and #13), as well as the fake block #13 and its QC. + //Since there is a time range conflict, and the low proof block is not a descendant of the high proof block, this is a proof of violation of rule #2. + mutable_variant_object valid_rule_2_proof_2 = prepare_rule_2_3_proof( light_client_data.active_finalizer_policy, + get_finality_block_data(real_chain_block_14_result), + get_finality_block_data(real_chain_block_15_result).qc_data.qc.value(), + light_client_data.get_reversible_blocks()[light_client_data.get_reversible_blocks().size()-1], //fake block #13 + light_client_data.get_current_block().qc_data.qc.value(), //QC over fake block #13 + valid_proof_2_target_reversible_block.first, + valid_proof_2_target_reversible_block.second, + block_14_reversible_blocks); + + shouldPass(real_chain, "rule2"_n, valid_rule_2_proof_2); + + //Fake chain resume production, catches up with real chain + auto fake_chain_block_15_result = light_client_data.scan_block(fake_chain.produce_block()); + + //Caught up + auto fake_chain_block_16_result = light_client_data.scan_block(fake_chain.produce_block()); + auto real_chain_block_16_result = real_chain.produce_block(); + + BOOST_TEST(fake_chain_block_16_result.qc_data.qc.has_value()); + BOOST_TEST(real_chain_block_16_result.qc_data.qc.has_value()); + + //We once again disable vote propagation on the real chain + real_chain.vote_propagation = {1,0,0}; + + //Produce a few more blocks on both chains + auto fake_chain_block_17_result = light_client_data.scan_block(fake_chain.produce_block()); + auto real_chain_block_17_result = real_chain.produce_block(); + + BOOST_TEST(fake_chain_block_17_result.qc_data.qc.has_value()); + BOOST_TEST(real_chain_block_17_result.qc_data.qc.has_value()); + + auto fake_chain_block_18_result = light_client_data.scan_block(fake_chain.produce_block()); + auto real_chain_block_18_result = real_chain.produce_block(); + + BOOST_TEST(fake_chain_block_18_result.qc_data.qc.has_value()); + BOOST_TEST(!real_chain_block_18_result.qc_data.qc.has_value()); + + //restore vote propagation on the real chain + real_chain.vote_propagation = {1,0,1}; + + auto fake_chain_block_19_result = light_client_data.scan_block(fake_chain.produce_block()); + auto real_chain_block_19_result = real_chain.produce_block(); + + //compute reversible digests on block #16, #17 and #18 on the real chain + std::vector block_19_reversible_blocks = { get_finality_block_data(real_chain_block_16_result), get_finality_block_data(real_chain_block_17_result), get_finality_block_data(real_chain_block_18_result)}; + + //calculate the merkle root of the reversible blocks digests + digest_type block_19_reversible_blocks_merkle_root = calculate_reversible_blocks_merkle(block_19_reversible_blocks); + + //verify the merkle root of the reversible blocks digests is the same as the one recorded by the light client + BOOST_TEST(real_chain_block_19_result.level_3_commitments.reversible_blocks_mroot == block_19_reversible_blocks_merkle_root); + + BOOST_TEST(fake_chain_block_19_result.qc_data.qc.has_value()); + BOOST_TEST(!real_chain_block_19_result.qc_data.qc.has_value()); + + //At this point, we can verify that the time range of the last fake chain block on which we have a QC is fully contained within the time range of the last real chain block + BOOST_TEST(fake_chain_block_18_result.level_3_commitments.latest_qc_claim_block_num == fake_chain_block_17_result.block->block_num()); + BOOST_TEST(real_chain_block_19_result.level_3_commitments.latest_qc_claim_block_num == real_chain_block_16_result.block->block_num()); + + auto valid_proof_3_target_reversible_block = get_target_reversible_block(block_19_reversible_blocks, fake_chain_block_17_result.block->block_num()); + + //stop producing on fake chain, produce one more block on the real chain to get a QC on previous block + auto real_chain_block_20_result = real_chain.produce_block(); + + BOOST_TEST(real_chain_block_20_result.qc_data.qc.has_value()); + + //We can now produce a proof of finality violation demonstrating that finalizers were locked on #18 on the fake chain, while also voting on a conflicting block #19 + //on the real chain which is not a descendant of #18, where the time range committed to by #18 is fully contained within the time range committed to by #19 + mutable_variant_object valid_rule_3_proof_1 = prepare_rule_2_3_proof( light_client_data.active_finalizer_policy, + get_finality_block_data(real_chain_block_19_result), + get_finality_block_data(real_chain_block_20_result).qc_data.qc.value(), + light_client_data.get_reversible_blocks()[light_client_data.get_reversible_blocks().size()-1], //fake block #18 + light_client_data.get_current_block().qc_data.qc.value(), //QC over fake block #18 + valid_proof_3_target_reversible_block.first, + valid_proof_3_target_reversible_block.second, + block_19_reversible_blocks); + + shouldPass(real_chain, "rule3"_n, valid_rule_3_proof_1); + + //Now, to ensure the smart contract rejects invalid proofs, we can test providing a proof from the real chain where the high proof block is a descendant of the + //low proof block + mutable_variant_object invalid_rule_3_proof_1 = prepare_rule_2_3_proof( light_client_data.active_finalizer_policy, + get_finality_block_data(real_chain_block_19_result), + get_finality_block_data(real_chain_block_20_result).qc_data.qc.value(), + get_finality_block_data(real_chain_block_16_result), //real block #16 + get_finality_block_data(real_chain_block_17_result).qc_data.qc.value(), //QC over fake block #16 + get_finality_block_data(real_chain_block_16_result), + 0, + {get_finality_block_data(real_chain_block_16_result)}); + + //The contract rejects the invalid proof + shouldFail(real_chain, "rule3"_n, invalid_rule_3_proof_1); + + //Resume production on fake chain + auto fake_chain_block_20_result = light_client_data.scan_block(fake_chain.produce_block()); + BOOST_TEST(fake_chain_block_20_result.qc_data.qc.has_value()); + + //Caught up + auto fake_chain_block_21_result = light_client_data.scan_block(fake_chain.produce_block()); + auto real_chain_block_21_result = real_chain.produce_block(); + + BOOST_TEST(fake_chain_block_21_result.qc_data.qc.has_value()); + BOOST_TEST(real_chain_block_21_result.qc_data.qc.has_value()); + + //We once again disable vote propagation on the fake chain + fake_chain.vote_propagation = {1,0,0}; + + //Produce a few more blocks on both chains + auto fake_chain_block_22_result = light_client_data.scan_block(fake_chain.produce_block()); + auto real_chain_block_22_result = real_chain.produce_block(); + + BOOST_TEST(fake_chain_block_22_result.qc_data.qc.has_value()); + BOOST_TEST(real_chain_block_22_result.qc_data.qc.has_value()); + + auto fake_chain_block_23_result = light_client_data.scan_block(fake_chain.produce_block()); + auto real_chain_block_23_result = real_chain.produce_block(); + + BOOST_TEST(!fake_chain_block_23_result.qc_data.qc.has_value()); + BOOST_TEST(real_chain_block_23_result.qc_data.qc.has_value()); + + //restore vote propagation on the fake chain + fake_chain.vote_propagation = {1,1,0}; + + auto fake_chain_block_24_result = light_client_data.scan_block(fake_chain.produce_block()); + auto real_chain_block_24_result = real_chain.produce_block(); + + BOOST_TEST(!fake_chain_block_24_result.qc_data.qc.has_value()); + BOOST_TEST(real_chain_block_24_result.qc_data.qc.has_value()); + + //At this point, we can verify that the time range of the last real chain block on which we have a QC is fully encapsulated within the time range of the last fake chain block + BOOST_TEST(fake_chain_block_24_result.level_3_commitments.latest_qc_claim_block_num == fake_chain_block_21_result.block->block_num()); + BOOST_TEST(real_chain_block_23_result.level_3_commitments.latest_qc_claim_block_num == real_chain_block_22_result.block->block_num()); + + //get the reversible blocks for block #24 (should be three blocks: #21, #22 and #23) + std::vector block_24_reversible_blocks = light_client_data.get_reversible_blocks(); + + + BOOST_TEST(block_24_reversible_blocks.size() == 3u); + + BOOST_TEST(compute_block_ref_digest(block_24_reversible_blocks[0]) == compute_block_ref_digest(fake_chain_block_21_result)); + BOOST_TEST(compute_block_ref_digest(block_24_reversible_blocks[1]) == compute_block_ref_digest(fake_chain_block_22_result)); + BOOST_TEST(compute_block_ref_digest(block_24_reversible_blocks[2]) == compute_block_ref_digest(fake_chain_block_23_result)); + + //calculate the merkle root of the reversible blocks digests + digest_type block_24_reversible_blocks_merkle_root = calculate_reversible_blocks_merkle(block_24_reversible_blocks); + + //verify the merkle root of the reversible blocks digests is the same as the one recorded by the light client + BOOST_TEST(fake_chain_block_24_result.level_3_commitments.reversible_blocks_mroot == block_24_reversible_blocks_merkle_root); + + //stop producing on real chain, produce one more block on the fake chain to get a QC on previous block + auto fake_chain_block_25_result = light_client_data.scan_block(fake_chain.produce_block()); + + BOOST_TEST(fake_chain_block_25_result.qc_data.qc.has_value()); + + auto valid_proof_4_target_reversible_block = get_target_reversible_block(block_24_reversible_blocks, fake_chain_block_22_result.block->block_num()); + + //We can now produce a proof of finality violation demonstrating that finalizers were locked on #23 on the real chain, while also voting on a conflicting block #24 + //on the fake chain which is not a descendant of #23, where the time range committed to by #23 is fully contained within the time range committed to by #24 + mutable_variant_object valid_rule_3_proof_2 = prepare_rule_2_3_proof( light_client_data.active_finalizer_policy, + light_client_data.get_reversible_blocks()[light_client_data.get_reversible_blocks().size()-1], //fake block #23 + light_client_data.get_current_block().qc_data.qc.value(), //QC over fake block #23 + get_finality_block_data(real_chain_block_23_result), + get_finality_block_data(real_chain_block_24_result).qc_data.qc.value(), + valid_proof_4_target_reversible_block.first, + valid_proof_4_target_reversible_block.second, + block_24_reversible_blocks); + + shouldPass(real_chain, "rule3"_n, valid_rule_3_proof_2); + + + } FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/unittests/svnn_ibc_tests.cpp b/unittests/svnn_ibc_tests.cpp index d0a0f3a982..4f47bb209b 100644 --- a/unittests/svnn_ibc_tests.cpp +++ b/unittests/svnn_ibc_tests.cpp @@ -18,45 +18,12 @@ using namespace eosio::testing; using mvo = mutable_variant_object; -std::string bitset_to_input_string(const boost::dynamic_bitset& bitset) { - static const char* hexchar = "0123456789abcdef"; - - boost::dynamic_bitset bs(bitset); - bs.resize((bs.size() + 7) & ~0x7); - assert(bs.size() % 8 == 0); - - std::string result; - result.resize(bs.size() / 4); - for (size_t i = 0; i < bs.size(); i += 4) { - size_t x = 0; - for (size_t j = 0; j < 4; ++j) - x += bs[i+j] << j; - auto slot = i / 4; - result[slot % 2 ? slot - 1 : slot + 1] = hexchar[x]; // flip the two hex digits for each byte - } - return result; -} - -std::string binary_to_hex(const std::string& bin) { - boost::dynamic_bitset bitset(bin.size()); - for (size_t i = 0; i < bin.size(); ++i) { - if (bin[i] == '1') { - bitset.set(bin.size() - 1 - i); - } - } - return bitset_to_input_string(bitset); -} - -auto active_finalizers_string = [](const finality_proof::ibc_block_data_t& bd) { - return bitset_to_input_string(bd.qc_data.qc.value().active_policy_sig.strong_votes.value()); -}; - BOOST_AUTO_TEST_SUITE(svnn_ibc) BOOST_AUTO_TEST_CASE(ibc_test) { try { // cluster is set up with the head about to produce IF Genesis - finality_proof::proof_test_cluster<4> cluster; + finality_proof::proof_test_cluster cluster; // produce IF Genesis block auto genesis_block_result = cluster.produce_block(); @@ -97,9 +64,9 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) auto block_5_result = cluster.produce_block(); //block num : 9 auto block_6_result = cluster.produce_block(); //block num : 10 - BOOST_TEST(block_4_result.qc_data.qc.has_value()); - BOOST_TEST(block_5_result.qc_data.qc.has_value()); - BOOST_TEST(block_6_result.qc_data.qc.has_value()); + BOOST_REQUIRE(block_4_result.qc_data.qc.has_value()); + BOOST_REQUIRE(block_5_result.qc_data.qc.has_value()); + BOOST_REQUIRE(block_6_result.qc_data.qc.has_value()); // create a few proofs we'll use to perform tests @@ -114,12 +81,12 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) ("major_version", 1) ("minor_version", 0) ("active_finalizer_policy_generation", 1) - ("witness_hash", block_3_result.level_2_commitments_digest) + ("witness_hash", block_3_result.level_2_commitments_digest()) ("finality_mroot", block_3_result.finality_root) ) ("active_policy_qc", mvo() ("signature", block_4_result.qc_data.qc.value().active_policy_sig.sig.to_string()) - ("finalizers", active_finalizers_string(block_4_result)) + ("strong_votes", finality_proof::finalizers_string(block_4_result.qc_data.qc.value().active_policy_sig.strong_votes.value())) ) ) ("target_block_proof_of_inclusion", mvo() @@ -130,7 +97,7 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) ("major_version", 1) ("minor_version", 0) ("active_finalizer_policy_generation", 1) - ("witness_hash", block_2_result.level_2_commitments_digest) + ("witness_hash", block_2_result.level_2_commitments_digest()) ("finality_mroot", block_2_result.finality_root) ) ("timestamp", block_2_result.block->timestamp) @@ -153,12 +120,12 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) ("major_version", 1) ("minor_version", 0) ("active_finalizer_policy_generation", 1) - ("witness_hash", block_3_result.level_2_commitments_digest) + ("witness_hash", block_3_result.level_2_commitments_digest()) ("finality_mroot", block_3_result.finality_root) ) ("active_policy_qc", mvo() ("signature", block_4_result.qc_data.qc.value().active_policy_sig.sig.to_string()) - ("finalizers", active_finalizers_string(block_4_result)) + ("strong_votes", finality_proof::finalizers_string(block_4_result.qc_data.qc.value().active_policy_sig.strong_votes.value())) ) ) ("target_block_proof_of_inclusion", mvo() @@ -188,12 +155,12 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) ("major_version", 1) ("minor_version", 0) ("active_finalizer_policy_generation", 1) - ("witness_hash", block_4_result.level_2_commitments_digest) + ("witness_hash", block_4_result.level_2_commitments_digest()) ("finality_mroot", block_4_result.finality_root) ) ("active_policy_qc", mvo() ("signature", block_5_result.qc_data.qc.value().active_policy_sig.sig.to_string()) - ("finalizers", active_finalizers_string(block_5_result)) + ("strong_votes", finality_proof::finalizers_string(block_5_result.qc_data.qc.value().active_policy_sig.strong_votes.value())) ) ) ("target_block_proof_of_inclusion", mvo() @@ -204,7 +171,7 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) ("major_version", 1) ("minor_version", 0) ("active_finalizer_policy_generation", 1) - ("witness_hash", block_2_result.level_2_commitments_digest) + ("witness_hash", block_2_result.level_2_commitments_digest()) ("finality_mroot", block_2_result.finality_root) ) ("timestamp", block_2_result.block->timestamp) @@ -230,7 +197,7 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) ("major_version", 1) ("minor_version", 0) ("active_finalizer_policy_generation", 1) - ("witness_hash", block_2_result.level_2_commitments_digest) + ("witness_hash", block_2_result.level_2_commitments_digest()) ("finality_mroot", block_2_result.finality_root) ) ("timestamp", block_2_result.block->timestamp) @@ -327,8 +294,8 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) // onblock action proof mutable_variant_object onblock_action_proof = mvo() - ("target_block_index", 0) - ("final_block_index", 3) + ("target_action_index", 0) + ("final_action_index", 3) ("target", mvo() ("action", mvo() ("account", block_7_result.onblock_trace.act.account) @@ -346,8 +313,8 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) // first action proof (check_heavy_proof_1) mutable_variant_object action_proof_1 = mvo() - ("target_block_index", 1) - ("final_block_index", 3) + ("target_action_index", 1) + ("final_action_index", 3) ("target", mvo() ("action", mvo() ("account", check_heavy_proof_1_trace.act.account) @@ -364,8 +331,8 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) // second action proof (check_light_proof_1) mutable_variant_object action_proof_2 = mvo() - ("target_block_index", 2) - ("final_block_index", 3) + ("target_action_index", 2) + ("final_action_index", 3) ("target", mvo() ("action", mvo() ("account", check_light_proof_1_trace.act.account) @@ -388,12 +355,12 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) ("major_version", 1) ("minor_version", 0) ("active_finalizer_policy_generation", 1) - ("witness_hash", block_8_result.level_2_commitments_digest) + ("witness_hash", block_8_result.level_2_commitments_digest()) ("finality_mroot", block_8_result.finality_root) ) ("active_policy_qc", mvo() ("signature", block_9_result.qc_data.qc.value().active_policy_sig.sig.to_string()) - ("finalizers", active_finalizers_string(block_9_result)) + ("strong_votes", finality_proof::finalizers_string(block_9_result.qc_data.qc.value().active_policy_sig.strong_votes.value())) ) ) ("target_block_proof_of_inclusion", mvo() @@ -404,7 +371,7 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) ("major_version", 1) ("minor_version", 0) ("active_finalizer_policy_generation", 1) - ("witness_hash", block_7_result.level_2_commitments_digest) + ("witness_hash", block_7_result.level_2_commitments_digest()) ("finality_mroot", block_7_result.finality_root) ) ("timestamp", block_7_result.block->timestamp) @@ -429,7 +396,7 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) ("major_version", 1) ("minor_version", 0) ("active_finalizer_policy_generation", 1) - ("witness_hash", block_7_result.level_2_commitments_digest) + ("witness_hash", block_7_result.level_2_commitments_digest()) ("finality_mroot", block_7_result.finality_root) ) ("timestamp", block_7_result.block->timestamp) @@ -490,13 +457,12 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) ("major_version", 1) ("minor_version", 0) ("active_finalizer_policy_generation", 1) - ("pending_finalizer_policy_generation", 2) - ("witness_hash", block_10_result.level_2_commitments_digest) + ("witness_hash", block_10_result.level_2_commitments_digest()) ("finality_mroot", block_10_result.finality_root) ) ("active_policy_qc", mvo() ("signature", block_11_result.qc_data.qc.value().active_policy_sig.sig.to_string()) - ("finalizers", active_finalizers_string(block_11_result)) + ("strong_votes", finality_proof::finalizers_string(block_11_result.qc_data.qc.value().active_policy_sig.strong_votes.value())) ) ) ("target_block_proof_of_inclusion", mvo() @@ -507,7 +473,7 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) ("major_version", 1) ("minor_version", 0) ("active_finalizer_policy_generation", 1) - ("witness_hash", block_9_result.level_2_commitments_digest) + ("witness_hash", block_9_result.level_2_commitments_digest()) ("finality_mroot", block_9_result.finality_root) ) ("timestamp", block_9_result.block->timestamp) @@ -549,17 +515,17 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) ("major_version", 1) ("minor_version", 0) ("active_finalizer_policy_generation", 1) - ("last_pending_finalizer_policy_generation", 2) - ("witness_hash", block_11_result.level_2_commitments_digest) + ("pending_finalizer_policy_generation", 2) + ("witness_hash", block_11_result.level_2_commitments_digest()) ("finality_mroot", block_11_result.finality_root) ) ("active_policy_qc", mvo() ("signature", block_12_result.qc_data.qc.value().active_policy_sig.sig.to_string()) - ("finalizers", active_finalizers_string(block_12_result)) + ("strong_votes", finality_proof::finalizers_string(block_12_result.qc_data.qc.value().active_policy_sig.strong_votes.value())) ) ("pending_policy_qc", mvo() ("signature", block_12_result.qc_data.qc.value().pending_policy_sig.value().sig.to_string()) - ("finalizers", active_finalizers_string(block_12_result)) + ("strong_votes", finality_proof::finalizers_string(block_12_result.qc_data.qc.value().pending_policy_sig.value().strong_votes.value())) ) ) ("target_block_proof_of_inclusion", mvo() @@ -570,9 +536,9 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) ("major_version", 1) ("minor_version", 0) ("active_finalizer_policy_generation", 1) - ("last_pending_finalizer_policy_generation", 2) - ("pending_finalizer_policy", cluster.last_pending_finalizer_policy) - ("witness_hash", block_10_result.level_3_commitments_digest) + ("pending_finalizer_policy_generation", 2) + ("last_pending_finalizer_policy", cluster.last_pending_finalizer_policy) + ("witness_hash", block_10_result.level_3_commitments_digest()) ("last_pending_finalizer_policy_start_timestamp", block_10_result.last_pending_finalizer_policy_start_timestamp ) ("finality_mroot", block_10_result.finality_root) ) @@ -603,12 +569,12 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) ("major_version", 1) ("minor_version", 0) ("active_finalizer_policy_generation", 2) - ("witness_hash", block_12_result.level_2_commitments_digest) + ("witness_hash", block_12_result.level_2_commitments_digest()) ("finality_mroot", block_12_result.finality_root) ) ("active_policy_qc", mvo() ("signature", block_13_result.qc_data.qc.value().active_policy_sig.sig.to_string()) - ("finalizers", active_finalizers_string(block_13_result)) + ("strong_votes", finality_proof::finalizers_string(block_13_result.qc_data.qc.value().active_policy_sig.strong_votes.value())) ) ) ("target_block_proof_of_inclusion", mvo() @@ -619,8 +585,8 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) ("major_version", 1) ("minor_version", 0) ("active_finalizer_policy_generation", 1) - ("last_pending_finalizer_policy_generation", 2) - ("witness_hash", block_11_result.level_2_commitments_digest) + ("pending_finalizer_policy_generation", 2) + ("witness_hash", block_11_result.level_2_commitments_digest()) ("finality_mroot", block_11_result.finality_root) ) ("timestamp", block_11_result.block->timestamp) @@ -648,16 +614,13 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) // The QC provided to prove this also proves a commitment from finalizers to this policy, so the smart contract can accept it. action_trace check_heavy_proof_4_trace = cluster.node0.push_action("ibc"_n, "checkproof"_n, "ibc"_n, heavy_proof_4)->action_traces[0]; - BOOST_CHECK(true); // now that we have successfully proven finalizer policy generation #2, the contract has it, and we can prove heavy_proof_5 action_trace check_heavy_proof_5_trace = cluster.node0.push_action("ibc"_n, "checkproof"_n, "ibc"_n, heavy_proof_5)->action_traces[0]; - BOOST_CHECK(true); // we now test light proof we should still be able to verify a proof of finality for block #2 without finality proof, // since the previous root is still cached cluster.node0.push_action("ibc"_n, "checkproof"_n, "ibc"_n, light_proof_1); - BOOST_CHECK(true); cluster.produce_blocks(10); //advance 5 seconds // the root is still cached when performing this action, so the action succeeds. @@ -665,7 +628,6 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) // so subsequent calls with the same action data will fail cluster.node0.push_action("ibc"_n, "checkproof"_n, "ibc"_n, light_proof_1); - BOOST_CHECK(true); cluster.produce_block(); //advance 1 block to avoid duplicate transaction last_action_failed = false; @@ -688,12 +650,14 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) chain.set_code( "ibc"_n, eosio::testing::test_contracts::ibc_wasm()); chain.set_abi( "ibc"_n, eosio::testing::test_contracts::ibc_abi()); - std::string bitset_1 = binary_to_hex("0"); - std::string bitset_2 = binary_to_hex("011"); - std::string bitset_3 = binary_to_hex("00011101010"); - std::string bitset_4 = binary_to_hex("11011000100001"); - std::string bitset_5 = binary_to_hex("111111111111111111111"); - std::string bitset_6 = binary_to_hex("000000111111111111111"); + std::string bitset_1 = finality_proof::binary_to_hex("0"); + std::string bitset_2 = finality_proof::binary_to_hex("011"); + std::string bitset_3 = finality_proof::binary_to_hex("00011101010"); + std::string bitset_4 = finality_proof::binary_to_hex("11011000100001"); + std::string bitset_5 = finality_proof::binary_to_hex("111111111111111111111"); + std::string bitset_6 = finality_proof::binary_to_hex("000000111111111111111"); + std::string bitset_7 = finality_proof::binary_to_hex("0011"); + std::string bitset_8 = finality_proof::binary_to_hex("1100"); chain.push_action("ibc"_n, "testbitset"_n, "ibc"_n, mvo() ("bitset_string", "00") @@ -702,35 +666,47 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) ); chain.push_action("ibc"_n, "testbitset"_n, "ibc"_n, mvo() - ("bitset_string", "30") //bitset bytes are reversed, so we do the same to test + ("bitset_string", "03") ("bitset_vector", bitset_2) ("finalizers_count", 3) ); - + chain.push_action("ibc"_n, "testbitset"_n, "ibc"_n, mvo() - ("bitset_string", "ae00") + ("bitset_string", "00ea") ("bitset_vector", bitset_3) ("finalizers_count", 11) ); chain.push_action("ibc"_n, "testbitset"_n, "ibc"_n, mvo() - ("bitset_string", "1263") + ("bitset_string", "3621") ("bitset_vector", bitset_4) ("finalizers_count", 14) ); chain.push_action("ibc"_n, "testbitset"_n, "ibc"_n, mvo() - ("bitset_string", "fffff1") + ("bitset_string", "1fffff") ("bitset_vector", bitset_5) ("finalizers_count", 21) ); chain.push_action("ibc"_n, "testbitset"_n, "ibc"_n, mvo() - ("bitset_string", "fff700") + ("bitset_string", "007fff") ("bitset_vector", bitset_6) ("finalizers_count", 21) ); + chain.push_action("ibc"_n, "testbitset"_n, "ibc"_n, mvo() + ("bitset_string", "03") + ("bitset_vector", bitset_7) + ("finalizers_count", 4) + ); + + chain.push_action("ibc"_n, "testbitset"_n, "ibc"_n, mvo() + ("bitset_string", "0c") + ("bitset_vector", bitset_8) + ("finalizers_count", 4) + ); + } FC_LOG_AND_RETHROW() } BOOST_AUTO_TEST_SUITE_END() diff --git a/unittests/test-contracts/savanna/CMakeLists.txt b/unittests/test-contracts/savanna/CMakeLists.txt index e4502b30f5..894f9c3cfb 100644 --- a/unittests/test-contracts/savanna/CMakeLists.txt +++ b/unittests/test-contracts/savanna/CMakeLists.txt @@ -1 +1,3 @@ -add_subdirectory( ibc ) \ No newline at end of file +add_subdirectory( ibc ) +add_subdirectory( finality_violation ) + diff --git a/unittests/test-contracts/savanna/common/bitset.hpp b/unittests/test-contracts/savanna/common/bitset.hpp index 5222f01032..9b00cf7661 100644 --- a/unittests/test-contracts/savanna/common/bitset.hpp +++ b/unittests/test-contracts/savanna/common/bitset.hpp @@ -63,8 +63,8 @@ namespace savanna { std::string result; result.reserve(data.size() * 2); // Each byte will be represented by two hex characters for (auto byte : data) { - result.push_back(hex_chars[byte & 0x0F]); - result.push_back(hex_chars[(byte >> 4) & 0x0F]); + result.insert(result.begin(), hex_chars[byte & 0x0F]); + result.insert(result.begin(), hex_chars[(byte >> 4) & 0x0F]); } return result; } diff --git a/unittests/test-contracts/savanna/common/savanna.hpp b/unittests/test-contracts/savanna/common/savanna.hpp index f6fcd083aa..b9c5e1fd74 100644 --- a/unittests/test-contracts/savanna/common/savanna.hpp +++ b/unittests/test-contracts/savanna/common/savanna.hpp @@ -12,11 +12,28 @@ using namespace eosio; namespace savanna { - struct quorum_certificate { - //representation of a bitset, where each bit represents the ordinal finalizer position according to canonical sorting rules of the finalizer policy - std::vector finalizers; - //string representation of a BLS signature - std::string signature; + constexpr std::array weak_bls_sig_postfix = { 'W', 'E', 'A', 'K' }; + using weak_digest_t = std::array; + + inline std::string create_weak_digest(const checksum256& digest) { + weak_digest_t res; + std::memcpy(res.begin(), digest.data(), digest.size()); + std::memcpy(res.begin() + digest.size(), weak_bls_sig_postfix.data(), weak_bls_sig_postfix.size()); + return std::string(res.begin(), res.end()); + } + + inline std::string create_strong_digest(const checksum256& digest){ + std::array fd_data = digest.extract_as_byte_array(); + return std::string(fd_data.begin(), fd_data.end()); + } + + struct quorum_certificate_input { + //representation of optional strong and weak bitsets, where each bit represents the ordinal finalizer position according to canonical sorting rules of the finalizer policy + std::optional> strong_votes; + std::optional> weak_votes; + + //string representation of a BLS signature + std::string signature; }; struct finalizer_authority_internal { @@ -61,7 +78,7 @@ namespace savanna { }; //Compute the maximum number of layers of a merkle tree for a given number of leaves - uint64_t calculate_max_depth(uint64_t node_count) { + uint64_t calculate_max_depth(const uint64_t node_count) { if(node_count <= 1) return node_count; return 64 - __builtin_clzll(2 << (64 - 1 - __builtin_clzll ((node_count - 1)))); //instead of std::bit_ceil available in C++ 20 @@ -71,7 +88,7 @@ namespace savanna { return __builtin_bswap32(input); } - checksum256 hash_pair(const std::pair p){ + checksum256 hash_pair(const std::pair& p){ auto result = eosio::pack(p); return sha256(result.data(), result.size()); } @@ -84,7 +101,7 @@ namespace savanna { } //compute path for proof of inclusion - std::vector _get_proof_path(uint64_t leaf_index, const uint64_t leaf_count) { + std::vector _get_proof_path(const uint64_t leaf_index, const uint64_t leaf_count) { std::vector proof_path; uint64_t current_leaf_count = leaf_count; uint64_t current_index = leaf_index; @@ -106,10 +123,11 @@ namespace savanna { } //compute the merkle root of target node and vector of merkle branches - checksum256 _compute_root(const std::vector proof_nodes, const checksum256& target, const uint64_t target_block_index, const uint64_t final_block_index){ + checksum256 _compute_root(const std::vector& proof_nodes, const checksum256& target, const uint64_t target_block_index, const uint64_t final_block_index){ checksum256 hash = target; std::vector proof_path = _get_proof_path(target_block_index, final_block_index+1); - check(proof_path.size() == proof_nodes.size(), "internal error"); //should not happen + + check(proof_path.size() == proof_nodes.size(), "proof path size and proof nodes size mismatch"); for (int i = 0 ; i < proof_nodes.size() ; i++){ const checksum256 node = proof_nodes[i]; hash = hash_pair(proof_path[i] ? std::make_pair(node, hash) : std::make_pair(hash, node)); @@ -123,50 +141,138 @@ namespace savanna { bls_g1_add(op1, op2, r); return r; } + + void check_duplicate_votes(const savanna::bitset& strong_votes, const savanna::bitset& weak_votes, const finalizer_policy_input& finalizer_policy){ + + auto fa_itr = finalizer_policy.finalizers.begin(); + auto fa_end_itr = finalizer_policy.finalizers.end(); + + size_t index = 0; + + while (fa_itr != fa_end_itr){ + bool voted_strong = strong_votes.test(index); + bool voted_weak = weak_votes.test(index); + check (!(voted_strong && voted_weak), "conflicting finalizer votes detected in QC"); + index++; + fa_itr++; + } - // verify signature - bool _verify(const std::string& public_key, const std::string& signature, const std::string& message){ - return bls_signature_verify(decode_bls_public_key_to_g1(public_key), decode_bls_signature_to_g2(signature), message); } - - //verify that the quorum certificate over the finality digest is valid - void _check_qc(const quorum_certificate& qc, const checksum256& finality_digest, const finalizer_policy_input finalizer_policy){ + + uint64_t aggregate_keys(const savanna::bitset& votes, const finalizer_policy_input& finalizer_policy, bls_g1& agg_pub_key){ + auto fa_itr = finalizer_policy.finalizers.begin(); auto fa_end_itr = finalizer_policy.finalizers.end(); - size_t finalizer_count = finalizer_policy.finalizers.size(); - savanna::bitset b(finalizer_count, qc.finalizers); bool first = true; size_t index = 0; uint64_t weight = 0; - bls_g1 agg_pub_key; - while (fa_itr != fa_end_itr){ - if (b.test(index)){ - bls_g1 pub_key = decode_bls_public_key_to_g1(fa_itr->public_key); - if (first){ - first=false; - agg_pub_key = pub_key; - } - else agg_pub_key = _g1add(agg_pub_key, pub_key); - weight+=fa_itr->weight; - } - index++; - fa_itr++; + if (votes.test(index)){ + bls_g1 pub_key = decode_bls_public_key_to_g1(fa_itr->public_key); + if (first){ + first=false; + agg_pub_key = pub_key; + } + else agg_pub_key = _g1add(agg_pub_key, pub_key); + weight+=fa_itr->weight; + } + index++; + fa_itr++; + } + + return weight; + + } + + void _verify(const std::vector& messages, const std::vector& agg_pub_keys, const std::string& agg_sig){ + + check(messages.size() == agg_pub_keys.size(), "messages vector and pub key vectors must be of the same size"); + + for (size_t i = 0 ; i < messages.size(); i++){ + //verify signature validity + check(bls_signature_verify(agg_pub_keys[i], decode_bls_signature_to_g2(agg_sig), messages[i]), "signature verification failed"); + } + + } + + //verify that the quorum certificate over the finality digest is valid + void _check_qc(const quorum_certificate_input& qc, const checksum256& finality_digest, const finalizer_policy_input& finalizer_policy, const bool count_strong_only, const bool enforce_threshold_check){ + + check(qc.strong_votes.has_value() || qc.weak_votes.has_value(), "quorum certificate must have at least one bitset"); + + size_t finalizer_count = finalizer_policy.finalizers.size(); + + uint64_t weight = 0; + + if (count_strong_only){ + bls_g1 agg_pub_key; + check(qc.strong_votes.has_value(), "required strong votes are missing"); + savanna::bitset strong_b(finalizer_count, qc.strong_votes.value()); + weight = aggregate_keys(strong_b, finalizer_policy, agg_pub_key); + _verify({create_strong_digest(finality_digest)}, {agg_pub_key}, qc.signature); + } + else { + + if (qc.strong_votes.has_value() && qc.weak_votes.has_value()){ + //weak QC (composed of strong and weak votes) + bls_g1 strong_agg_pub_key; + bls_g1 weak_agg_pub_key; + savanna::bitset strong_b(finalizer_count, qc.strong_votes.value()); + savanna::bitset weak_b(finalizer_count, qc.weak_votes.value()); + check_duplicate_votes(strong_b, weak_b, finalizer_policy); + uint64_t strong_weight = aggregate_keys(strong_b, finalizer_policy, strong_agg_pub_key); + uint64_t weak_weight = aggregate_keys(strong_b, finalizer_policy, weak_agg_pub_key); + _verify({create_strong_digest(finality_digest), create_weak_digest(finality_digest)}, {strong_agg_pub_key, weak_agg_pub_key}, qc.signature); + weight=strong_weight+weak_weight; + } + else if (qc.weak_votes.has_value()){ + //weak QC (composed of weak votes) + bls_g1 agg_pub_key; + savanna::bitset weak_b(finalizer_count, qc.weak_votes.value()); + weight = aggregate_keys(weak_b, finalizer_policy, agg_pub_key); + _verify({create_weak_digest(finality_digest)}, {agg_pub_key}, qc.signature); + } + else { + //strong QC + bls_g1 agg_pub_key; + savanna::bitset strong_b(finalizer_count, qc.strong_votes.value()); + weight = aggregate_keys(strong_b, finalizer_policy, agg_pub_key); + _verify({create_strong_digest(finality_digest)}, {agg_pub_key}, qc.signature); + } + } - //verify that we have enough vote weight to meet the quorum threshold of the target policy - check(weight>=finalizer_policy.threshold, "insufficient signatures to reach quorum"); - std::array fd_data = finality_digest.extract_as_byte_array(); - std::string message(fd_data.begin(), fd_data.end()); + if (enforce_threshold_check) check(weight>=finalizer_policy.threshold, "insufficient signatures to reach quorum"); - std::string s_agg_pub_key = encode_g1_to_bls_public_key(agg_pub_key); - //verify signature validity - check(_verify(s_agg_pub_key, qc.signature, message), "signature verification failed"); } + checksum256 get_merkle_root(const std::vector& leaves) { + std::vector> tree; + + tree.push_back(leaves); + + std::vector current_level = leaves; + while (current_level.size() > 1) { + std::vector next_level; + for (size_t i = 0; i < current_level.size(); i += 2) { + checksum256 left = current_level[i]; + if (i + 1 < current_level.size()) { + checksum256 right = current_level[i + 1]; + next_level.push_back(hash_pair(std::make_pair(left, right))); + } else { + next_level.push_back(left); + } + } + tree.insert(tree.begin(), next_level); // Prepend to build the tree upwards + current_level = next_level; + } + + return current_level.front(); + + } struct authseq { name account; @@ -227,8 +333,8 @@ namespace savanna { }; struct action_proof_of_inclusion { - uint64_t target_block_index = 0; - uint64_t final_block_index = 0; + uint64_t target_action_index = 0; + uint64_t final_action_index = 0; action_data target; @@ -237,18 +343,35 @@ namespace savanna { //returns the merkle root obtained by hashing target.digest() with merkle_branches checksum256 root() const { checksum256 digest = action_data_internal(target).digest(); - checksum256 root = _compute_root(merkle_branches, digest, target_block_index, final_block_index); + checksum256 root = _compute_root(merkle_branches, digest, target_action_index, final_action_index); return root; }; }; - struct level_3_commitments_t { + // commitments used in the context of finality violation proofs, minus the base digest + struct level_3_commitments_input { checksum256 reversible_blocks_mroot{}; uint32_t latest_qc_claim_block_num{0}; checksum256 latest_qc_claim_finality_digest{}; block_timestamp_type latest_qc_claim_timestamp; block_timestamp_type timestamp; - checksum256 base_digest{}; + }; + + struct level_3_commitments_t : level_3_commitments_input { + checksum256 base_digest; + + level_3_commitments_t(const level_3_commitments_input& base, const checksum256& _base_digest) : + level_3_commitments_input(base), + base_digest(_base_digest){ + } + + EOSLIB_SERIALIZE(level_3_commitments_t, + (reversible_blocks_mroot) + (latest_qc_claim_block_num) + (latest_qc_claim_finality_digest) + (latest_qc_claim_timestamp) + (timestamp) + (base_digest)) }; // commitments used in the context of finalizer policy transitions @@ -258,6 +381,36 @@ namespace savanna { checksum256 l3_commitments_digest{}; }; + struct block_ref_data { + uint32_t block_num{0}; + block_timestamp_type timestamp; + checksum256 finality_digest; + block_timestamp_type parent_timestamp; + + checksum256 digest() const { + auto result = eosio::pack(*this); + checksum256 hash = sha256(result.data(), result.size()); + return hash; + }; + + }; + + struct reversible_proof_of_inclusion { + uint64_t target_reversible_block_index = 0; + uint64_t final_reversible_block_index = 0; + + block_ref_data target; + + std::vector merkle_branches; + + //returns the merkle root obtained by hashing target.digest() with merkle_branches + checksum256 root() const { + checksum256 digest = target.digest(); + checksum256 root = _compute_root(merkle_branches, digest, target_reversible_block_index, final_reversible_block_index); + return root; + }; + }; + struct dynamic_data_v0 { //block_num is always present uint32_t block_num = 0; @@ -297,14 +450,16 @@ namespace savanna { //finalizer_policy_generation for this block uint32_t active_finalizer_policy_generation; - std::optional last_pending_finalizer_policy_generation; + std::optional pending_finalizer_policy_generation; //Allows the contract to obtain knowledge about them and to record them in its internal state. - std::optional pending_finalizer_policy; + std::optional last_pending_finalizer_policy; std::optional last_pending_finalizer_policy_start_timestamp; - //if finality violation info is present (not implemented yet), witness_hash should be the base digest. + std::optional level_3_commitments; + + //if level_3_commitments is present, witness_hash should be the base digest. //if finalizer policy transition info is present, witness_hash should be the level 3 commitments digest. //Otherwise, witness_hash should be level 2 commitments digest checksum256 witness_hash; @@ -315,20 +470,44 @@ namespace savanna { //resolves witness hash if it needs to be calculated checksum256 resolve_witness() const { - //todo : add support for finality violation proofs + checksum256 l3_digest; + + if (level_3_commitments.has_value()){ + + check(last_pending_finalizer_policy.has_value() + && last_pending_finalizer_policy_start_timestamp.has_value() + && witness_hash!=checksum256(), "must provide full level 2 commitments when providing level 3 commitments"); + + //finality violation proofs + auto l3_commitments = level_3_commitments.value(); + + auto l3_input = level_3_commitments_input{ + .reversible_blocks_mroot = l3_commitments.reversible_blocks_mroot, + .latest_qc_claim_block_num = l3_commitments.latest_qc_claim_block_num, + .latest_qc_claim_finality_digest = l3_commitments.latest_qc_claim_finality_digest, + .latest_qc_claim_timestamp = l3_commitments.latest_qc_claim_timestamp, + .timestamp = l3_commitments.timestamp + }; - //finalizer policy transition proofs + auto l3_packed = eosio::pack(level_3_commitments_t{ l3_input, witness_hash}); - if (pending_finalizer_policy.has_value() + l3_digest = sha256(l3_packed.data(), l3_packed.size()); + + } + else l3_digest = witness_hash; + + if (last_pending_finalizer_policy.has_value() && last_pending_finalizer_policy_start_timestamp.has_value() && witness_hash!=checksum256()){ - checksum256 policy_digest = pending_finalizer_policy.value().digest(); + //finalizer policy transition information + + checksum256 policy_digest = last_pending_finalizer_policy.value().digest(); auto l2_packed = eosio::pack(level_2_commitments_t{ .last_pending_fin_pol_digest = policy_digest, .last_pending_fin_pol_start_timestamp = last_pending_finalizer_policy_start_timestamp.value(), - .l3_commitments_digest = witness_hash + .l3_commitments_digest = l3_digest }); checksum256 l2_digest = sha256(l2_packed.data(), l2_packed.size()); @@ -351,7 +530,7 @@ namespace savanna { block_finality_data_internal(const block_finality_data& base) : block_finality_data(base){ resolved_witness_hash = base.resolve_witness(); - resolved_last_pending_finalizer_policy_generation = base.last_pending_finalizer_policy_generation.has_value() ? base.last_pending_finalizer_policy_generation.value() : active_finalizer_policy_generation; + resolved_last_pending_finalizer_policy_generation = base.pending_finalizer_policy_generation.has_value() ? base.pending_finalizer_policy_generation.value() : active_finalizer_policy_generation; } checksum256 finality_digest() const { @@ -361,6 +540,7 @@ namespace savanna { } EOSLIB_SERIALIZE(block_finality_data_internal, (major_version)(minor_version)(active_finalizer_policy_generation)(resolved_last_pending_finalizer_policy_generation)(finality_mroot)(resolved_witness_hash)) + }; //used in "heavy" proofs, where verification of finality digest is performed @@ -466,10 +646,10 @@ namespace savanna { block_finality_data qc_block; //signature over finality_digest() of qc_block by active policy generation - quorum_certificate active_policy_qc; + quorum_certificate_input active_policy_qc; //signature over finality_digest() of qc_block by pending policy generation (required during transitions, prohibited otherwise) - std::optional pending_policy_qc; + std::optional pending_policy_qc; }; diff --git a/unittests/test-contracts/savanna/finality_violation/CMakeLists.txt b/unittests/test-contracts/savanna/finality_violation/CMakeLists.txt new file mode 100644 index 0000000000..33b00c9ce1 --- /dev/null +++ b/unittests/test-contracts/savanna/finality_violation/CMakeLists.txt @@ -0,0 +1,9 @@ +set( FINALITY_VIOLATION_CONTRACT "finality_violation" ) + +if( EOSIO_COMPILE_TEST_CONTRACTS ) + include_directories(${CMAKE_CURRENT_SOURCE_DIR}/common) + add_contract( ${FINALITY_VIOLATION_CONTRACT} ${FINALITY_VIOLATION_CONTRACT} ${FINALITY_VIOLATION_CONTRACT}.cpp ) +else() + configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/${FINALITY_VIOLATION_CONTRACT}.wasm ${CMAKE_CURRENT_BINARY_DIR}/${FINALITY_VIOLATION_CONTRACT}.wasm COPYONLY ) + configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/${FINALITY_VIOLATION_CONTRACT}.abi ${CMAKE_CURRENT_BINARY_DIR}/${FINALITY_VIOLATION_CONTRACT}.abi COPYONLY ) +endif() diff --git a/unittests/test-contracts/savanna/finality_violation/finality_violation.abi b/unittests/test-contracts/savanna/finality_violation/finality_violation.abi new file mode 100644 index 0000000000..863ac2d24d --- /dev/null +++ b/unittests/test-contracts/savanna/finality_violation/finality_violation.abi @@ -0,0 +1,301 @@ +{ + "____comment": "This file was generated with eosio-abigen. DO NOT EDIT ", + "version": "eosio::abi/1.2", + "types": [], + "structs": [ + { + "name": "block_finality_data", + "base": "", + "fields": [ + { + "name": "major_version", + "type": "uint32" + }, + { + "name": "minor_version", + "type": "uint32" + }, + { + "name": "active_finalizer_policy_generation", + "type": "uint32" + }, + { + "name": "pending_finalizer_policy_generation", + "type": "uint32?" + }, + { + "name": "last_pending_finalizer_policy", + "type": "finalizer_policy_input?" + }, + { + "name": "last_pending_finalizer_policy_start_timestamp", + "type": "block_timestamp_type?" + }, + { + "name": "level_3_commitments", + "type": "level_3_commitments_input?" + }, + { + "name": "witness_hash", + "type": "checksum256" + }, + { + "name": "finality_mroot", + "type": "checksum256" + } + ] + }, + { + "name": "block_ref_data", + "base": "", + "fields": [ + { + "name": "block_num", + "type": "uint32" + }, + { + "name": "timestamp", + "type": "block_timestamp_type" + }, + { + "name": "finality_digest", + "type": "checksum256" + }, + { + "name": "parent_timestamp", + "type": "block_timestamp_type" + } + ] + }, + { + "name": "finality_proof", + "base": "", + "fields": [ + { + "name": "qc_block", + "type": "block_finality_data" + }, + { + "name": "active_policy_qc", + "type": "quorum_certificate_input" + }, + { + "name": "pending_policy_qc", + "type": "quorum_certificate_input?" + } + ] + }, + { + "name": "finalizer_authority_input", + "base": "", + "fields": [ + { + "name": "description", + "type": "string" + }, + { + "name": "weight", + "type": "uint64" + }, + { + "name": "public_key", + "type": "string" + } + ] + }, + { + "name": "finalizer_policy_input", + "base": "", + "fields": [ + { + "name": "generation", + "type": "uint32" + }, + { + "name": "threshold", + "type": "uint64" + }, + { + "name": "finalizers", + "type": "finalizer_authority_input[]" + } + ] + }, + { + "name": "level_3_commitments_input", + "base": "", + "fields": [ + { + "name": "reversible_blocks_mroot", + "type": "checksum256" + }, + { + "name": "latest_qc_claim_block_num", + "type": "uint32" + }, + { + "name": "latest_qc_claim_finality_digest", + "type": "checksum256" + }, + { + "name": "latest_qc_claim_timestamp", + "type": "block_timestamp_type" + }, + { + "name": "timestamp", + "type": "block_timestamp_type" + } + ] + }, + { + "name": "pair_string_string", + "base": "", + "fields": [ + { + "name": "first", + "type": "string" + }, + { + "name": "second", + "type": "string" + } + ] + }, + { + "name": "quorum_certificate_input", + "base": "", + "fields": [ + { + "name": "strong_votes", + "type": "bytes?" + }, + { + "name": "weak_votes", + "type": "bytes?" + }, + { + "name": "signature", + "type": "string" + } + ] + }, + { + "name": "reversible_proof_of_inclusion", + "base": "", + "fields": [ + { + "name": "target_reversible_block_index", + "type": "uint64" + }, + { + "name": "final_reversible_block_index", + "type": "uint64" + }, + { + "name": "target", + "type": "block_ref_data" + }, + { + "name": "merkle_branches", + "type": "checksum256[]" + } + ] + }, + { + "name": "rule1", + "base": "", + "fields": [ + { + "name": "finalizer_policy", + "type": "finalizer_policy_input" + }, + { + "name": "proof_1", + "type": "finality_proof" + }, + { + "name": "proof_2", + "type": "finality_proof" + } + ] + }, + { + "name": "rule2", + "base": "", + "fields": [ + { + "name": "finalizer_policy", + "type": "finalizer_policy_input" + }, + { + "name": "high_proof", + "type": "finality_proof" + }, + { + "name": "low_proof", + "type": "finality_proof" + }, + { + "name": "reversible_proof_of_inclusion", + "type": "reversible_proof_of_inclusion" + } + ] + }, + { + "name": "rule3", + "base": "", + "fields": [ + { + "name": "finalizer_policy", + "type": "finalizer_policy_input" + }, + { + "name": "high_proof", + "type": "finality_proof" + }, + { + "name": "low_proof", + "type": "finality_proof" + }, + { + "name": "reversible_proof_of_inclusion", + "type": "reversible_proof_of_inclusion" + } + ] + } + ], + "actions": [ + { + "name": "rule1", + "type": "rule1", + "ricardian_contract": "" + }, + { + "name": "rule2", + "type": "rule2", + "ricardian_contract": "" + }, + { + "name": "rule3", + "type": "rule3", + "ricardian_contract": "" + } + ], + "tables": [], + "ricardian_clauses": [], + "variants": [], + "action_results": [ + { + "name": "rule1", + "result_type": "pair_string_string" + }, + { + "name": "rule2", + "result_type": "pair_string_string" + }, + { + "name": "rule3", + "result_type": "pair_string_string" + } + ] +} \ No newline at end of file diff --git a/unittests/test-contracts/savanna/finality_violation/finality_violation.cpp b/unittests/test-contracts/savanna/finality_violation/finality_violation.cpp new file mode 100644 index 0000000000..79cea6dce5 --- /dev/null +++ b/unittests/test-contracts/savanna/finality_violation/finality_violation.cpp @@ -0,0 +1,190 @@ +#include "finality_violation.hpp" + +savanna::bitset merge_bitsets(const savanna::bitset& bitset_1, const savanna::bitset& bitset_2){ + + //check that bitsets are of the same size + check(bitset_1.size()==bitset_2.size(), "cannot merge bitsets of different sizes"); + + //create a new bitset of the same size as the input bitsets + savanna::bitset result_bitset(bitset_1.size()); + + //merge the bitsets by setting the bits that are set in either of the input bitsets into the result bitset + for (size_t i = 0 ; i < bitset_1.size(); i++){ + if (bitset_1.test(i) || bitset_2.test(i)) result_bitset.set(i); + } + return result_bitset; +} + +savanna::bitset create_bitset(const size_t finalizers_count, const std::optional>& strong_votes, const std::optional>& weak_votes){ + + //check that at least one set of votes is present + check(strong_votes.has_value() || weak_votes.has_value(), "must have at least one set of votes to create a bitset"); + + savanna::bitset result_bitset(finalizers_count); + + //if both strong and weak votes are present, merge them + if (strong_votes.has_value() && weak_votes.has_value()){ + + savanna::bitset strong_bitset(finalizers_count, strong_votes.value()); + savanna::bitset weak_bitset(finalizers_count, weak_votes.value()); + + return merge_bitsets(strong_bitset, weak_bitset); + + } + //if only strong votes are present, use them + else if (strong_votes.has_value()) return savanna::bitset(finalizers_count, strong_votes.value()); + //if only weak votes are present, use them + else return savanna::bitset(finalizers_count, weak_votes.value()); + +} + + +std::pair check_bitsets(const finalizer_policy_input& finalizer_policy, const finality_proof& high_proof, const finality_proof& low_proof, const bool high_proof_strong_votes_only = false, const bool low_proof_strong_votes_only = false){ + + std::optional> hsv = high_proof.active_policy_qc.strong_votes; + std::optional> lsv = low_proof.active_policy_qc.strong_votes; + + std::optional> hwv = high_proof.active_policy_qc.weak_votes; + std::optional> lwv = low_proof.active_policy_qc.weak_votes; + + //if bitset verification applies only to strong votes, remove weak votes + if (high_proof_strong_votes_only) hwv = std::nullopt; + if (low_proof_strong_votes_only) lwv = std::nullopt; + + //create bitsets + savanna::bitset high_proof_bitset = create_bitset(finalizer_policy.finalizers.size(), hsv, hwv); + savanna::bitset low_proof_bitset = create_bitset(finalizer_policy.finalizers.size(), lsv, lwv); + + //compare bitsets + auto result = bitset::compare(high_proof_bitset, low_proof_bitset); + + return result; + +} + +//Verify QCs presented as proof +std::pair check_qcs( const finalizer_policy_input& finalizer_policy, const finality_proof& proof_1, const finality_proof& proof_2){ + + //Verify we have our level 3 commitments for both proofs + check(proof_1.qc_block.level_3_commitments.has_value(), "level 3 commitments structure must be present in both proofs to prove a finality violation"); + check(proof_2.qc_block.level_3_commitments.has_value(), "level 3 commitments structure must be present in both proofs to prove a finality violation"); + + //Compute finality digests for both proofs + checksum256 digest_1 = block_finality_data_internal(proof_1.qc_block).finality_digest(); + checksum256 digest_2 = block_finality_data_internal(proof_2.qc_block).finality_digest(); + + check(digest_1 != digest_2, "finality digests must be different"); + + //Verify QC signatures over the finality digests + _check_qc(proof_1.active_policy_qc, digest_1, finalizer_policy, false, false); + _check_qc(proof_2.active_policy_qc, digest_2, finalizer_policy, false, false); + + return {digest_1, digest_2}; + +} + +//Rule #1 : Do not vote on different blocks with the same timestamp +std::pair finality_violation::rule1(const finalizer_policy_input& finalizer_policy, const finality_proof& proof_1, const finality_proof& proof_2){ + + //Verify QCs + check_qcs(finalizer_policy, proof_1, proof_2); + + //Compare timestamps + block_timestamp timestamp_1 = proof_1.qc_block.level_3_commitments.value().timestamp; + block_timestamp timestamp_2 = proof_2.qc_block.level_3_commitments.value().timestamp; + + check(timestamp_1 == timestamp_2, "proofs must be over blocks that have the same timestamp"); + + //Proof of rule #1 finality violation + + auto result = check_bitsets(finalizer_policy, proof_1, proof_2); + + return {result.first.to_string(), result.second.to_string()}; + +} + +//Rule #2 : Do not vote on a block that conflicts with the time interval of a strong vote +std::pair finality_violation::rule2( const finalizer_policy_input& finalizer_policy, + const finality_proof& high_proof, + const finality_proof& low_proof, + const reversible_proof_of_inclusion& proof_of_inclusion){ + + //Verify QCs + auto finalizer_digests = check_qcs(finalizer_policy, high_proof, low_proof); + + //Compare timestamps + block_timestamp high_proof_timestamp = high_proof.qc_block.level_3_commitments.value().timestamp; + block_timestamp low_proof_timestamp = low_proof.qc_block.level_3_commitments.value().timestamp; + + block_timestamp high_proof_parent_timestamp = proof_of_inclusion.target.parent_timestamp; + + //Verify that the proof of inclusion resolves to the reversible blocks mroot of the high proof + check(proof_of_inclusion.root() == high_proof.qc_block.level_3_commitments.value().reversible_blocks_mroot, "proof of inclusion must resolve to the reversible blocks mroot of the high proof"); + + //A time range conflict has occured if the high proof timestamp is greater than or equal to the low proof timestamp and the high proof parent timestamp is less than the low proof timestamp + bool time_range_conflict = high_proof_parent_timestamp < low_proof_timestamp && high_proof_timestamp >= low_proof_timestamp; + check(time_range_conflict, "proofs must demonstrate a conflicting time range"); + + bool finality_violation = false; + + //If the timestamp for the submitted reversible blocks leaf node is strictly greater than low_proof_timestamp, we know that the low proof block is not an ancestor of the high proof block + //and therefore, a rule 2 violation has occurred. + if(proof_of_inclusion.target.timestamp > low_proof_timestamp) finality_violation = true; + else if(proof_of_inclusion.target.timestamp == low_proof_timestamp) { + //If the timestamp for the submitted reversible blocks leaf node is exactly equal to low_proof_timestamp, we need to compare the finality digest of the low proof block to the finality digest of the submitted reversible blocks leaf node, + //to check that they are not the same. If they are the same, the submitted proof is not correct. But if they are different, then we know that the low proof block is not an ancestor of the high proof block + check(finalizer_digests.second != proof_of_inclusion.target.finality_digest, "finality digest of low proof must be different from the finality digest of the submitted reversible blocks leaf node"); + finality_violation = true; + } + + check(finality_violation, "proofs must demonstrate a finality violation"); + + //Proof of rule #2 finality violation + auto result = check_bitsets(finalizer_policy, high_proof, low_proof, false, true); + + return {result.first.to_string(), result.second.to_string()}; + +} + +//Rule #3 : Do not vote on a block that conflicts with another block on which you are locked +std::pair finality_violation::rule3( const finalizer_policy_input& finalizer_policy, + const finality_proof& high_proof, + const finality_proof& low_proof, + const reversible_proof_of_inclusion& proof_of_inclusion){ + + //Verify QCs + auto finalizer_digests = check_qcs(finalizer_policy, high_proof, low_proof); + + //Compare timestamps + block_timestamp target_proof_timestamp = proof_of_inclusion.target.timestamp; + block_timestamp low_proof_last_claim_timestamp = low_proof.qc_block.level_3_commitments.value().latest_qc_claim_timestamp; + + block_timestamp target_proof_parent_timestamp = proof_of_inclusion.target.parent_timestamp; + + //Verify that the proof of inclusion resolves to the reversible blocks mroot of the high proof + check(proof_of_inclusion.root() == high_proof.qc_block.level_3_commitments.value().reversible_blocks_mroot, "proof of inclusion must resolve to the reversible blocks mroot of the high proof"); + + //A lock violation has occured if the high proof timestamp is greater than or equal to the low proof last claim timestamp and the high proof parent timestamp is less than the low proof last claim timestamp + bool lock_violation = target_proof_timestamp >= low_proof_last_claim_timestamp && target_proof_parent_timestamp < low_proof_last_claim_timestamp; + check(lock_violation, "proofs must demonstrate a lock violation"); + + bool finality_violation = false; + + //If the timestamp for the submitted reversible blocks leaf node is strictly greater than low_proof_last_claim_timestamp, we know that the low proof block is not an ancestor of the high proof block + //and therefore, a rule 3 violation has occurred. + if(target_proof_timestamp > low_proof_last_claim_timestamp) finality_violation = true; + else if(target_proof_timestamp == low_proof_last_claim_timestamp) { + //If the timestamp for the submitted reversible blocks leaf node is exactly equal to low_proof_timestamp, we need to compare the finality digest of the low proof block to the finality digest of the submitted reversible blocks leaf node, + //to check that they are not the same. If they are the same, the submitted proof is not correct. But if they are different, then we know that the low proof block is not an ancestor of the high proof block + check(finalizer_digests.second != proof_of_inclusion.target.finality_digest, "finality digest of low proof must be different from the finality digest of the submitted reversible blocks leaf node"); + finality_violation = true; + } + + check(finality_violation, "proofs must demonstrate a finality violation"); + + //Proof of rule #3 finality violation + auto result = check_bitsets(finalizer_policy, high_proof, low_proof, true, false); + + return {result.first.to_string(), result.second.to_string()}; + +} diff --git a/unittests/test-contracts/savanna/finality_violation/finality_violation.hpp b/unittests/test-contracts/savanna/finality_violation/finality_violation.hpp new file mode 100644 index 0000000000..92658af192 --- /dev/null +++ b/unittests/test-contracts/savanna/finality_violation/finality_violation.hpp @@ -0,0 +1,39 @@ +#include +#include +#include +#include +#include + +#include "../common/savanna.hpp" + +using namespace eosio; +using namespace savanna; + +CONTRACT finality_violation : public contract { + public: + using contract::contract; + + //Rule #1 : Do not vote on different blocks with the same timestamp + [[eosio::action]] + std::pair rule1( const finalizer_policy_input& finalizer_policy, const finality_proof& proof_1, const finality_proof& proof_2); + + //Rule #2 : Do not vote on a block that conflicts with the time interval of a strong vote + [[eosio::action]] + std::pair rule2( const finalizer_policy_input& finalizer_policy, + const finality_proof& high_proof, + const finality_proof& low_proof, + const reversible_proof_of_inclusion& reversible_proof_of_inclusion); + + //Rule #3 : Do not vote on a block that conflicts with another block on which you are locked + [[eosio::action]] + std::pair rule3( const finalizer_policy_input& finalizer_policy, + const finality_proof& high_proof, + const finality_proof& low_proof, + const reversible_proof_of_inclusion& reversible_proof_of_inclusion); + + + + // compare two bitsets + static std::pair compare_qc(const quorum_certificate_input& qc1, const quorum_certificate_input& qc2); + +}; \ No newline at end of file diff --git a/unittests/test-contracts/savanna/finality_violation/finality_violation.wasm b/unittests/test-contracts/savanna/finality_violation/finality_violation.wasm new file mode 100755 index 0000000000..f367c6b246 Binary files /dev/null and b/unittests/test-contracts/savanna/finality_violation/finality_violation.wasm differ diff --git a/unittests/test-contracts/savanna/ibc/ibc.abi b/unittests/test-contracts/savanna/ibc/ibc.abi index 5ce49e0c63..12ad8e14a4 100644 --- a/unittests/test-contracts/savanna/ibc/ibc.abi +++ b/unittests/test-contracts/savanna/ibc/ibc.abi @@ -67,11 +67,11 @@ "base": "", "fields": [ { - "name": "target_block_index", + "name": "target_action_index", "type": "uint64" }, { - "name": "final_block_index", + "name": "final_action_index", "type": "uint64" }, { @@ -101,17 +101,21 @@ "type": "uint32" }, { - "name": "last_pending_finalizer_policy_generation", + "name": "pending_finalizer_policy_generation", "type": "uint32?" }, { - "name": "pending_finalizer_policy", + "name": "last_pending_finalizer_policy", "type": "finalizer_policy_input?" }, { "name": "last_pending_finalizer_policy_start_timestamp", "type": "block_timestamp_type?" }, + { + "name": "level_3_commitments", + "type": "level_3_commitments_input?" + }, { "name": "witness_hash", "type": "checksum256" @@ -204,11 +208,11 @@ }, { "name": "active_policy_qc", - "type": "quorum_certificate" + "type": "quorum_certificate_input" }, { "name": "pending_policy_qc", - "type": "quorum_certificate?" + "type": "quorum_certificate_input?" } ] }, @@ -266,6 +270,32 @@ } ] }, + { + "name": "level_3_commitments_input", + "base": "", + "fields": [ + { + "name": "reversible_blocks_mroot", + "type": "checksum256" + }, + { + "name": "latest_qc_claim_block_num", + "type": "uint32" + }, + { + "name": "latest_qc_claim_finality_digest", + "type": "checksum256" + }, + { + "name": "latest_qc_claim_timestamp", + "type": "block_timestamp_type" + }, + { + "name": "timestamp", + "type": "block_timestamp_type" + } + ] + }, { "name": "permission_level", "base": "", @@ -295,12 +325,16 @@ ] }, { - "name": "quorum_certificate", + "name": "quorum_certificate_input", "base": "", "fields": [ { - "name": "finalizers", - "type": "bytes" + "name": "strong_votes", + "type": "bytes?" + }, + { + "name": "weak_votes", + "type": "bytes?" }, { "name": "signature", diff --git a/unittests/test-contracts/savanna/ibc/ibc.cpp b/unittests/test-contracts/savanna/ibc/ibc.cpp index 4331c7198b..3c49d963a0 100644 --- a/unittests/test-contracts/savanna/ibc/ibc.cpp +++ b/unittests/test-contracts/savanna/ibc/ibc.cpp @@ -95,19 +95,19 @@ void ibc::_check_finality_proof(const finality_proof& finality_proof, const bloc finalizer_policy_input finalizer_policy = _get_stored_finalizer_policy(finality_proof.qc_block.active_finalizer_policy_generation); //verify QC. If QC is valid, it means that we have reached finality on the block referenced by the finality_mroot - _check_qc(finality_proof.active_policy_qc, block_finality_data_internal(finality_proof.qc_block).finality_digest(), finalizer_policy); + _check_qc(finality_proof.active_policy_qc, block_finality_data_internal(finality_proof.qc_block).finality_digest(), finalizer_policy, true, true); - if (finality_proof.qc_block.last_pending_finalizer_policy_generation.has_value()){ + if (finality_proof.qc_block.pending_finalizer_policy_generation.has_value()){ check(std::holds_alternative(target_block_proof_of_inclusion.target), "must provide extended data for transition blocks"); auto target = std::get(target_block_proof_of_inclusion.target); - check(target.finality_data.pending_finalizer_policy.has_value(), "must provide pending finalizer policy for transition blocks"); + check(target.finality_data.last_pending_finalizer_policy.has_value(), "must provide pending finalizer policy for transition blocks"); - _check_qc(finality_proof.pending_policy_qc.value(), block_finality_data_internal(finality_proof.qc_block).finality_digest(), target.finality_data.pending_finalizer_policy.value()); + _check_qc(finality_proof.pending_policy_qc.value(), block_finality_data_internal(finality_proof.qc_block).finality_digest(), target.finality_data.last_pending_finalizer_policy.value(), true, true); - _maybe_set_finalizer_policy(target.finality_data.pending_finalizer_policy.value(), target.dynamic_data.block_num); + _maybe_set_finalizer_policy(target.finality_data.last_pending_finalizer_policy.value(), target.dynamic_data.block_num); } @@ -123,7 +123,7 @@ void ibc::_check_finality_proof(const finality_proof& finality_proof, const bloc } -void ibc::_check_target_block_proof_of_inclusion(const block_proof_of_inclusion& proof, const std::optional reference_root){ +void ibc::_check_target_block_proof_of_inclusion(const block_proof_of_inclusion& proof, const std::optional& reference_root){ //resolve the proof to its merkle root checksum256 finality_mroot = proof.root(); @@ -174,7 +174,7 @@ ACTION ibc::checkproof(const proof& proof){ } -ACTION ibc::testbitset(const std::string bitset_string, const std::vector bitset_vector, const uint32_t finalizers_count){ +ACTION ibc::testbitset(const std::string& bitset_string, const std::vector& bitset_vector, const uint32_t finalizers_count){ savanna::bitset b(finalizers_count, bitset_vector); check(b.to_string() == bitset_string, "bitset mismatch"); diff --git a/unittests/test-contracts/savanna/ibc/ibc.hpp b/unittests/test-contracts/savanna/ibc/ibc.hpp index 3f535fe231..e4a95f2591 100644 --- a/unittests/test-contracts/savanna/ibc/ibc.hpp +++ b/unittests/test-contracts/savanna/ibc/ibc.hpp @@ -60,11 +60,11 @@ CONTRACT ibc : public contract { finalizer_policy_input _get_stored_finalizer_policy(const uint64_t finalizer_policy_generation); void _check_finality_proof(const finality_proof& finality_proof, const block_proof_of_inclusion& target_block_proof_of_inclusion); - void _check_target_block_proof_of_inclusion(const block_proof_of_inclusion& proof, const std::optional reference_root); + void _check_target_block_proof_of_inclusion(const block_proof_of_inclusion& proof, const std::optional& reference_root); ACTION setfpolicy(const finalizer_policy_input& policy, const uint32_t from_block_num); //set finality policy ACTION checkproof(const proof& proof); - ACTION testbitset(const std::string bitset_string, const std::vector bitset_vector, const uint32_t finalizers_count); + ACTION testbitset(const std::string& bitset_string, const std::vector& bitset_vector, const uint32_t finalizers_count); }; \ No newline at end of file diff --git a/unittests/test-contracts/savanna/ibc/ibc.wasm b/unittests/test-contracts/savanna/ibc/ibc.wasm index c1aafc3841..add846387f 100755 Binary files a/unittests/test-contracts/savanna/ibc/ibc.wasm and b/unittests/test-contracts/savanna/ibc/ibc.wasm differ diff --git a/unittests/test_contracts.hpp.in b/unittests/test_contracts.hpp.in index 64d3d26890..99d175f93e 100644 --- a/unittests/test_contracts.hpp.in +++ b/unittests/test_contracts.hpp.in @@ -51,6 +51,7 @@ namespace eosio { MAKE_READ_WASM_ABI(get_block_num_test, get_block_num_test, test-contracts) MAKE_READ_WASM_ABI(nested_container_multi_index, nested_container_multi_index, test-contracts) MAKE_READ_WASM_ABI(ibc, ibc, test-contracts/savanna) + MAKE_READ_WASM_ABI(finality_violation, finality_violation, test-contracts/savanna) }; } /// eosio::testing