diff --git a/libraries/chain/block_header_state.cpp b/libraries/chain/block_header_state.cpp index f3786aaad9..4dffecdd9f 100644 --- a/libraries/chain/block_header_state.cpp +++ b/libraries/chain/block_header_state.cpp @@ -11,15 +11,6 @@ namespace eosio::chain { // gets updated if a protocol feature causes a breaking change to light block // header validation -// data for finality_digest -struct finality_digest_data_v1 { - uint32_t major_version{light_header_protocol_version_major}; - uint32_t minor_version{light_header_protocol_version_minor}; - uint32_t active_finalizer_policy_generation {0}; - digest_type finality_tree_digest; - digest_type active_finalizer_policy_and_base_digest; -}; - // compute base_digest explicitly because of pointers involved. digest_type block_header_state::compute_base_digest() const { digest_type::encoder enc; @@ -298,6 +289,3 @@ block_header_state block_header_state::next(const signed_block_header& h, valida } // namespace eosio::chain -FC_REFLECT( eosio::chain::finality_digest_data_v1, - (major_version)(minor_version)(active_finalizer_policy_generation) - (finality_tree_digest)(active_finalizer_policy_and_base_digest) ) diff --git a/libraries/chain/include/eosio/chain/block_header_state.hpp b/libraries/chain/include/eosio/chain/block_header_state.hpp index 71eaf8f167..9347c97b57 100644 --- a/libraries/chain/include/eosio/chain/block_header_state.hpp +++ b/libraries/chain/include/eosio/chain/block_header_state.hpp @@ -20,6 +20,16 @@ namespace detail { struct schedule_info; }; constexpr uint32_t light_header_protocol_version_major = 1; constexpr uint32_t light_header_protocol_version_minor = 0; +// data for finality_digest +struct finality_digest_data_v1 { + uint32_t major_version{light_header_protocol_version_major}; + uint32_t minor_version{light_header_protocol_version_minor}; + uint32_t active_finalizer_policy_generation {0}; + digest_type finality_tree_digest; + digest_type active_finalizer_policy_and_base_digest; + +}; + // ------------------------------------------------------------------------------------------ // this is used for tracking in-flight `finalizer_policy` changes, which have been requested, // but are not activated yet. This struct is associated to a block_number in the @@ -128,3 +138,5 @@ FC_REFLECT( eosio::chain::block_header_state, (block_id)(header) (activated_protocol_features)(core)(active_finalizer_policy) (active_proposer_policy)(proposer_policies)(finalizer_policies) (finalizer_policy_generation)(header_exts)) + +FC_REFLECT( eosio::chain::finality_digest_data_v1, (major_version)(minor_version)(active_finalizer_policy_generation)(finality_tree_digest)(active_finalizer_policy_and_base_digest) ) diff --git a/unittests/finality_test_cluster.cpp b/unittests/finality_test_cluster.cpp index 3bebb3c5ef..0a49e0fba3 100644 --- a/unittests/finality_test_cluster.cpp +++ b/unittests/finality_test_cluster.cpp @@ -8,8 +8,6 @@ finality_test_cluster::finality_test_cluster() { setup_node(node1, "node1"_n); setup_node(node2, "node2"_n); - produce_and_push_block(); // make setfinalizer irreversible - // node0's votes node0.node.control->voted_block().connect( [&]( const eosio::chain::vote_signal_params& v ) { last_vote_status = std::get<1>(v); @@ -26,6 +24,24 @@ finality_test_cluster::finality_test_cluster() { node2.votes.emplace_back(std::get<2>(v)); }); +} + +void finality_test_cluster::initial_tests(){ + + auto block_1_n0 = node0.node.produce_block(); + auto block_1_n1 = node1.node.produce_block(); + auto block_1_n2 = node2.node.produce_block(); + + // this block contains the header exten/sion for the instant finality + std::optional ext = block_1_n0->extract_header_extension(eosio::chain::instant_finality_extension::extension_id()); + BOOST_TEST(!!ext); + std::optional fin_policy = std::get(*ext).new_finalizer_policy; + BOOST_TEST(!!fin_policy); + BOOST_TEST(fin_policy->finalizers.size() == 3); + BOOST_TEST(fin_policy->generation == 1); + + produce_and_push_block(); // make setfinalizer irreversible + // form a 3-chain to make LIB advacing on node0 // node0's vote (internal voting) and node1's vote make the quorum for (auto i = 0; i < 3; ++i) { @@ -46,6 +62,7 @@ finality_test_cluster::finality_test_cluster() { n.votes.clear(); n.prev_lib_num = n.node.control->if_irreversible_block_num(); } + } eosio::chain::vote_status finality_test_cluster::wait_on_vote(uint32_t connection_id, bool duplicate) { @@ -61,13 +78,14 @@ eosio::chain::vote_status finality_test_cluster::wait_on_vote(uint32_t connectio FC_ASSERT(false, "Duplicate should not have been signaled"); } return duplicate ? eosio::chain::vote_status::duplicate : last_vote_status.load(); -} +} // node0 produces a block and pushes it to node1 and node2 -void finality_test_cluster::produce_and_push_block() { +eosio::chain::signed_block_ptr finality_test_cluster::produce_and_push_block() { auto b = node0.node.produce_block(); node1.node.push_block(b); node2.node.push_block(b); + return b; } // send node1's vote identified by "vote_index" in the collected votes @@ -128,6 +146,13 @@ bool finality_test_cluster::produce_blocks_and_verify_lib_advancing() { return true; } +void finality_test_cluster::produce_blocks(uint32_t blocks_count) { + for (uint32_t i = 0; i < blocks_count; ++i) { + produce_and_push_block(); + process_node1_vote(); + } +} + void finality_test_cluster::node1_corrupt_vote_block_id() { std::lock_guard g(node1.votes_mtx); node1_orig_vote = node1.votes[0]; @@ -194,15 +219,6 @@ void finality_test_cluster::setup_node(node_info& node, eosio::chain::account_na FC_ASSERT( priv_keys.size() == 1, "number of private keys should be 1" ); node.priv_key = priv_keys[0]; // we only have one private key - auto block = node.node.produce_block(); - - // this block contains the header extension for the instant finality - std::optional ext = block->extract_header_extension(eosio::chain::instant_finality_extension::extension_id()); - BOOST_TEST(!!ext); - std::optional fin_policy = std::get(*ext).new_finalizer_policy; - BOOST_TEST(!!fin_policy); - BOOST_TEST(fin_policy->finalizers.size() == 3); - BOOST_TEST(fin_policy->generation == 1); } // send a vote to node0 @@ -235,4 +251,4 @@ eosio::chain::vote_status finality_test_cluster::process_vote(node_info& node, v vote_index = node.votes.size() - 1; g.unlock(); return process_vote( node, vote_index, mode ); -} +} \ No newline at end of file diff --git a/unittests/finality_test_cluster.hpp b/unittests/finality_test_cluster.hpp index 404bc4be5a..74f561b085 100644 --- a/unittests/finality_test_cluster.hpp +++ b/unittests/finality_test_cluster.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -29,11 +30,22 @@ class finality_test_cluster { weak, }; + struct node_info { + eosio::testing::tester node; + uint32_t prev_lib_num{0}; + std::mutex votes_mtx; + std::vector votes; + fc::crypto::blslib::bls_private_key priv_key; + }; + // Construct a test network and activate IF. finality_test_cluster(); // node0 produces a block and pushes it to node1 and node2 - void produce_and_push_block(); + eosio::chain::signed_block_ptr produce_and_push_block(); + + // make setfinalizer final and test finality + void initial_tests(); // send node1's vote identified by "index" in the collected votes eosio::chain::vote_status process_node1_vote(uint32_t vote_index, vote_mode mode = vote_mode::strong, bool duplicate = false); @@ -46,7 +58,7 @@ class finality_test_cluster { // send node2's latest vote eosio::chain::vote_status process_node2_vote(vote_mode mode = vote_mode::strong); - + // returns true if node0's LIB has advanced bool node0_lib_advancing(); @@ -61,6 +73,9 @@ class finality_test_cluster { // node1_votes and node2_votes when starting. bool produce_blocks_and_verify_lib_advancing(); + // Produces and propagate finality votes block_count blocks. + void produce_blocks(uint32_t blocks_count); + // Intentionally corrupt node1's vote's block_id and save the original vote void node1_corrupt_vote_block_id(); @@ -73,24 +88,16 @@ class finality_test_cluster { // Restore node1's original vote void node1_restore_to_original_vote(); -private: - - struct node_info { - eosio::testing::tester node; - uint32_t prev_lib_num{0}; - std::mutex votes_mtx; - std::vector votes; - fc::crypto::blslib::bls_private_key priv_key; - }; - - std::atomic last_connection_vote{0}; - std::atomic last_vote_status{}; - std::array nodes; node_info& node0 = nodes[0]; node_info& node1 = nodes[1]; node_info& node2 = nodes[2]; +private: + + std::atomic last_connection_vote{0}; + std::atomic last_vote_status{}; + eosio::chain::vote_message_ptr node1_orig_vote; // sets up "node_index" node diff --git a/unittests/finality_tests.cpp b/unittests/finality_tests.cpp index 6c056c0330..763d3c2ce3 100644 --- a/unittests/finality_tests.cpp +++ b/unittests/finality_tests.cpp @@ -8,7 +8,8 @@ BOOST_AUTO_TEST_SUITE(finality_tests) // verify LIB advances with 2 finalizers voting. BOOST_AUTO_TEST_CASE(two_votes) { try { finality_test_cluster cluster; - + cluster.initial_tests(); + for (auto i = 0; i < 3; ++i) { // node0 produces a block and pushes to node1 and node2 cluster.produce_and_push_block(); @@ -26,6 +27,7 @@ BOOST_AUTO_TEST_CASE(two_votes) { try { // verify LIB does not advances with finalizers not voting. BOOST_AUTO_TEST_CASE(no_votes) { try { finality_test_cluster cluster; + cluster.initial_tests(); cluster.produce_and_push_block(); cluster.node0_lib_advancing(); // reset @@ -47,7 +49,8 @@ BOOST_AUTO_TEST_CASE(no_votes) { try { // verify LIB advances with all of the three finalizers voting BOOST_AUTO_TEST_CASE(all_votes) { try { finality_test_cluster cluster; - + cluster.initial_tests(); + cluster.produce_and_push_block(); for (auto i = 0; i < 3; ++i) { // process node1 and node2's votes @@ -66,7 +69,8 @@ BOOST_AUTO_TEST_CASE(all_votes) { try { // verify LIB advances when votes conflict (strong first and followed by weak) BOOST_AUTO_TEST_CASE(conflicting_votes_strong_first) { try { finality_test_cluster cluster; - + cluster.initial_tests(); + cluster.produce_and_push_block(); for (auto i = 0; i < 3; ++i) { cluster.process_node1_vote(); // strong @@ -82,7 +86,8 @@ BOOST_AUTO_TEST_CASE(conflicting_votes_strong_first) { try { // verify LIB advances when votes conflict (weak first and followed by strong) BOOST_AUTO_TEST_CASE(conflicting_votes_weak_first) { try { finality_test_cluster cluster; - + cluster.initial_tests(); + cluster.produce_and_push_block(); for (auto i = 0; i < 3; ++i) { cluster.process_node1_vote(finality_test_cluster::vote_mode::weak); // weak @@ -98,6 +103,7 @@ BOOST_AUTO_TEST_CASE(conflicting_votes_weak_first) { try { // Verify a delayed vote works BOOST_AUTO_TEST_CASE(one_delayed_votes) { try { finality_test_cluster cluster; + cluster.initial_tests(); // hold the vote for the first block to simulate delay cluster.produce_and_push_block(); @@ -131,6 +137,7 @@ BOOST_AUTO_TEST_CASE(one_delayed_votes) { try { // Verify 3 consecutive delayed votes work BOOST_AUTO_TEST_CASE(three_delayed_votes) { try { finality_test_cluster cluster; + cluster.initial_tests(); // produce 4 blocks and hold the votes for the first 3 to simulate delayed votes // The 4 blocks have the same QC claim as no QCs are created because missing one vote @@ -171,7 +178,8 @@ BOOST_AUTO_TEST_CASE(three_delayed_votes) { try { BOOST_AUTO_TEST_CASE(out_of_order_votes) { try { finality_test_cluster cluster; - + cluster.initial_tests(); + // produce 3 blocks and hold the votes to simulate delayed votes // The 3 blocks have the same QC claim as no QCs are created because missing votes for (auto i = 0; i < 3; ++i) { @@ -211,7 +219,8 @@ BOOST_AUTO_TEST_CASE(out_of_order_votes) { try { // Verify a vote which was delayed by a large number of blocks does not cause any issues BOOST_AUTO_TEST_CASE(long_delayed_votes) { try { finality_test_cluster cluster; - + cluster.initial_tests(); + // Produce and push a block, vote on it after a long delay. constexpr uint32_t delayed_vote_index = 0; cluster.produce_and_push_block(); @@ -244,7 +253,8 @@ BOOST_AUTO_TEST_CASE(long_delayed_votes) { try { BOOST_AUTO_TEST_CASE(lost_votes) { try { finality_test_cluster cluster; - + cluster.initial_tests(); + // Produce and push a block, never vote on it to simulate lost. // The block contains a strong QC extension for prior block cluster.produce_and_push_block(); @@ -270,7 +280,8 @@ BOOST_AUTO_TEST_CASE(lost_votes) { try { BOOST_AUTO_TEST_CASE(one_weak_vote) { try { finality_test_cluster cluster; - + cluster.initial_tests(); + // Produce and push a block cluster.produce_and_push_block(); // Change the vote to a weak vote and process it @@ -303,6 +314,7 @@ BOOST_AUTO_TEST_CASE(one_weak_vote) { try { BOOST_AUTO_TEST_CASE(two_weak_votes) { try { finality_test_cluster cluster; + cluster.initial_tests(); // Produce and push a block cluster.produce_and_push_block(); @@ -340,6 +352,7 @@ BOOST_AUTO_TEST_CASE(two_weak_votes) { try { BOOST_AUTO_TEST_CASE(intertwined_weak_votes) { try { finality_test_cluster cluster; + cluster.initial_tests(); cluster.produce_and_push_block(); BOOST_REQUIRE(cluster.node2_lib_advancing()); @@ -385,7 +398,8 @@ BOOST_AUTO_TEST_CASE(intertwined_weak_votes) { try { // Verify a combination of weak, delayed, lost votes still work BOOST_AUTO_TEST_CASE(weak_delayed_lost_vote) { try { finality_test_cluster cluster; - + cluster.initial_tests(); + cluster.produce_and_push_block(); BOOST_REQUIRE(cluster.node2_lib_advancing()); BOOST_REQUIRE(cluster.node1_lib_advancing()); @@ -431,7 +445,8 @@ BOOST_AUTO_TEST_CASE(weak_delayed_lost_vote) { try { // Verify a combination of delayed, weak, lost votes still work BOOST_AUTO_TEST_CASE(delayed_strong_weak_lost_vote) { try { finality_test_cluster cluster; - + cluster.initial_tests(); + // A delayed vote (index 0) constexpr uint32_t delayed_index = 0; cluster.produce_and_push_block(); @@ -478,7 +493,8 @@ BOOST_AUTO_TEST_CASE(delayed_strong_weak_lost_vote) { try { // verify duplicate votes do not affect LIB advancing BOOST_AUTO_TEST_CASE(duplicate_votes) { try { finality_test_cluster cluster; - + cluster.initial_tests(); + cluster.produce_and_push_block(); for (auto i = 0; i < 5; ++i) { cluster.process_node1_vote(i, finality_test_cluster::vote_mode::strong); @@ -495,6 +511,7 @@ BOOST_AUTO_TEST_CASE(duplicate_votes) { try { // verify unknown_proposal votes are handled properly BOOST_AUTO_TEST_CASE(unknown_proposal_votes) { try { finality_test_cluster cluster; + cluster.initial_tests(); // node0 produces a block and pushes to node1 cluster.produce_and_push_block(); @@ -519,6 +536,7 @@ BOOST_AUTO_TEST_CASE(unknown_proposal_votes) { try { // verify unknown finalizer_key votes are handled properly BOOST_AUTO_TEST_CASE(unknown_finalizer_key_votes) { try { finality_test_cluster cluster; + cluster.initial_tests(); // node0 produces a block and pushes to node1 cluster.produce_and_push_block(); @@ -542,7 +560,8 @@ BOOST_AUTO_TEST_CASE(unknown_finalizer_key_votes) { try { // verify corrupted signature votes are handled properly BOOST_AUTO_TEST_CASE(corrupted_signature_votes) { try { finality_test_cluster cluster; - + cluster.initial_tests(); + // node0 produces a block and pushes to node1 cluster.produce_and_push_block(); diff --git a/unittests/svnn_ibc_tests.cpp b/unittests/svnn_ibc_tests.cpp index f1d1fe998a..b243166f02 100644 --- a/unittests/svnn_ibc_tests.cpp +++ b/unittests/svnn_ibc_tests.cpp @@ -1,136 +1,358 @@ #include -#include #include -#include - #include #include #include #include - #include "fork_test_utilities.hpp" +#include + +#include "finality_test_cluster.hpp" + using namespace eosio::chain; using namespace eosio::testing; -#include using mvo = mutable_variant_object; - BOOST_AUTO_TEST_SUITE(svnn_ibc) -// Extending the default chain tester -// ( libraries/testing/include/eosio/testing/tester.hpp ) -class ibc_tester : public tester { -public: - const account_name _bridge = "bridge"_n; - - // This is mostly for creating accounts and loading contracts. - void setup(){ - // load bridge contract - create_account( _bridge ); - set_code( _bridge, eosio::testing::test_contracts::svnn_ibc_wasm()); - set_abi( _bridge, eosio::testing::test_contracts::svnn_ibc_abi()); + //extract instant finality data from block header extension, as well as qc data from block extension + qc_data_t extract_qc_data(const signed_block_ptr& b) { + std::optional qc_data; + auto hexts = b->validate_and_extract_header_extensions(); + if (auto if_entry = hexts.lower_bound(instant_finality_extension::extension_id()); if_entry != hexts.end()) { + auto& if_ext = std::get(if_entry->second); + + // get the matching qc extension if present + auto exts = b->validate_and_extract_extensions(); + if (auto entry = exts.lower_bound(quorum_certificate_extension::extension_id()); entry != exts.end()) { + auto& qc_ext = std::get(entry->second); + return qc_data_t{ std::move(qc_ext.qc), if_ext.qc_claim }; + } + return qc_data_t{ {}, if_ext.qc_claim }; + } + return {}; + } + + //generate a proof of inclusion for a node at index from a list of leaves + std::vector generate_proof_of_inclusion(const std::vector leaves, const size_t index) { + + auto _leaves = leaves; + auto _index = index; + + std::vector merkle_branches; + + while (_leaves.size()>1){ + std::vector new_level; + for (size_t i = 0 ; i < _leaves.size() ; i+=2){ + digest_type left = _leaves[i]; + + if (i + 1 < _leaves.size() && (i + 1 != _leaves.size() - 1 || _leaves.size() % 2 == 0)){ + // Normal case: both children exist and are not at the end or are even + digest_type right = _leaves[i+1]; + + new_level.push_back(fc::sha256::hash(std::pair(left, right))); + if (_index == i || _index == i + 1) { + merkle_branches.push_back(_index == i ? right : left); + _index = i / 2; // Update index for next level + + } + } + else { + // Odd number of leaves at this level, and we're at the end + new_level.push_back(left); // Promote the left (which is also the right in this case) + if (_index == i) _index = i / 2; // Update index for next level, no sibling to add + + } + } + _leaves = new_level; + } + return merkle_branches; } - //set a finalizer policy - void set_policy(){ - auto cr = push_action( _bridge, "setfpolicy"_n, _bridge, mutable_variant_object() + BOOST_AUTO_TEST_CASE(ibc_test) { try { + + // cluster is set up with the head about to produce IF Genesis + finality_test_cluster cluster; + + // produce IF Genesis block + auto genesis_block = cluster.produce_and_push_block(); + + // ensure out of scope setup and initial cluster wiring is consistent + BOOST_CHECK(genesis_block->block_num() == 6); + + // check if IF Genesis block contains an IF extension + std::optional maybe_genesis_if_ext = genesis_block->extract_header_extension(eosio::chain::instant_finality_extension::extension_id()); + BOOST_CHECK(maybe_genesis_if_ext.has_value()); + + eosio::chain::block_header_extension genesis_if_ext = maybe_genesis_if_ext.value(); + + // check that the header extension is of the correct type + BOOST_CHECK(std::holds_alternative(genesis_if_ext)); + + // and that it has the expected initial finalizer_policy + std::optional maybe_active_finalizer_policy = std::get(genesis_if_ext).new_finalizer_policy; + + BOOST_CHECK(maybe_active_finalizer_policy.has_value()); + + eosio::chain::finalizer_policy active_finalizer_policy = maybe_active_finalizer_policy.value(); + + BOOST_CHECK(active_finalizer_policy.finalizers.size() == 3); + BOOST_CHECK(active_finalizer_policy.generation == 1); + + // compute the digest of the finalizer policy + auto active_finalizer_policy_digest = fc::sha256::hash(active_finalizer_policy); + + auto genesis_block_fd = cluster.node0.node.control->head_finality_data(); + + // verify we have finality data for the IF genesis block + BOOST_CHECK(genesis_block_fd.has_value()); + + // compute IF finality leaf + auto genesis_base_digest = genesis_block_fd.value().base_digest; + auto genesis_afp_base_digest = fc::sha256::hash(std::pair(active_finalizer_policy_digest, genesis_base_digest)); + + auto genesis_block_finality_digest = fc::sha256::hash(eosio::chain::finality_digest_data_v1{ + .active_finalizer_policy_generation = active_finalizer_policy.generation, + .finality_tree_digest = digest_type(), //nothing to finalize yet + .active_finalizer_policy_and_base_digest = genesis_afp_base_digest + }); + + // action_mroot computed using the post-IF activation merkle tree rules + auto genesis_block_action_mroot = genesis_block_fd.value().action_mroot; + + // initial finality leaf + auto genesis_block_leaf = fc::sha256::hash(valid_t::finality_leaf_node_t{ + .block_num = genesis_block->block_num(), + .finality_digest = genesis_block_finality_digest, + .action_mroot = genesis_block_action_mroot + }); + + // create the ibc account and deploy the ibc contract to it + cluster.node0.node.create_account( "ibc"_n ); + cluster.node0.node.set_code( "ibc"_n, eosio::testing::test_contracts::svnn_ibc_wasm()); + cluster.node0.node.set_abi( "ibc"_n, eosio::testing::test_contracts::svnn_ibc_abi()); + + cluster.node0.node.push_action( "ibc"_n, "setfpolicy"_n, "ibc"_n, mvo() ("from_block_num", 1) - ("policy", mutable_variant_object() - ("generation", 1) - ("fthreshold", 3) + ("policy", mvo() + ("generation", active_finalizer_policy.generation) + ("threshold", active_finalizer_policy.threshold) ("last_block_num", 0) - ("finalizers", fc::variants({ - mutable_variant_object() - ("description","finalizer1") - ("fweight", 1) - ("public_key", "b12eba13063c6cdc7bbe40e7de62a1c0f861a9ad55e924cdd5049be9b58e205053968179cede5be79afdcbbb90322406aefb7a5ce64edc2a4482d8656daed1eeacfb4286f661c0f9117dcd83fad451d301b2310946e5cd58808f7b441b280a02") - , - mutable_variant_object() - ("description","finalizer2") - ("fweight", 1) - ("public_key", "0728121cffe7b8ddac41817c3a6faca76ae9de762d9c26602f936ac3e283da756002d3671a2858f54c355f67b31b430b23b957dba426d757eb422db617be4cc13daf41691aa059b0f198fa290014d3c3e4fa1def2abc6a3328adfa7705c75508") - , - mutable_variant_object() - ("description","finalizer3") - ("fweight", 1) - ("public_key", "e06c31c83f70b4fe9507877563bfff49235774d94c98dbf9673d61d082ef589f7dd4865281f37d60d1bb433514d4ef0b787424fb5e53472b1d45d28d90614fad29a4e5e0fe70ea387f7845e22c843f6061f9be20a7af21d8b72d02f4ca494a0a") - , - mutable_variant_object() - ("description","finalizer4") - ("fweight", 1) - ("public_key", "08c9bd408bac02747e493d918e4b3e6bd1a2ffaf9bfca4f2e79dd22e12556bf46e911f25613c24d9f6403996c5246c19ef94aff48094868425eda1e46bcd059c59f3b060521be797f5cc2e6debe2180efa12c0814618a38836a64c3d7440740f") - })) + ("finalizers", active_finalizer_policy.finalizers) ) ); - } - //verify a proof - void check_proof(){ - auto cr = push_action( _bridge, "checkproof"_n, _bridge, mutable_variant_object() - ("proof", mutable_variant_object() - ("finality_proof", mutable_variant_object() - ("qc_block", mutable_variant_object() + // Transition block. Finalizers are not expected to vote on this block. + auto block_1 = cluster.produce_and_push_block(); + auto block_1_fd = cluster.node0.node.control->head_finality_data(); + auto block_1_action_mroot = block_1_fd.value().action_mroot; + auto block_1_finality_digest = cluster.node0.node.control->get_strong_digest_by_id(block_1->calculate_id()); + auto block_1_leaf = fc::sha256::hash(valid_t::finality_leaf_node_t{ + .block_num = block_1->block_num(), + .finality_digest = block_1_finality_digest, + .action_mroot = block_1_action_mroot + }); + + // Proper IF Block. From now on, finalizers must vote. Moving forward, the header action_mroot field is reconverted to provide the finality_mroot. The action_mroot is instead provided via the finality data + auto block_2 = cluster.produce_and_push_block(); + cluster.process_node1_vote(); //enough to reach quorum threshold + auto block_2_fd = cluster.node0.node.control->head_finality_data(); + auto block_2_action_mroot = block_2_fd.value().action_mroot; + auto block_2_base_digest = block_2_fd.value().base_digest; + auto block_2_finality_digest = cluster.node0.node.control->get_strong_digest_by_id(block_2->calculate_id()); + auto block_2_afp_base_digest = fc::sha256::hash(std::pair(active_finalizer_policy_digest, block_2_base_digest)); + auto block_2_leaf = fc::sha256::hash(valid_t::finality_leaf_node_t{ + .block_num = block_2->block_num(), + .finality_digest = block_2_finality_digest, + .action_mroot = block_2_action_mroot + }); + auto block_2_finality_root = block_2->action_mroot; + + // block_3 contains a QC over block_2 + auto block_3 = cluster.produce_and_push_block(); + cluster.process_node1_vote(); + auto block_3_fd = cluster.node0.node.control->head_finality_data(); + auto block_3_action_mroot = block_3_fd.value().action_mroot; + auto block_3_finality_digest = cluster.node0.node.control->get_strong_digest_by_id(block_3->calculate_id()); + auto block_3_leaf = fc::sha256::hash(valid_t::finality_leaf_node_t{ + .block_num = block_3->block_num(), + .finality_digest = block_3_finality_digest, + .action_mroot = block_3_action_mroot + }); + + // block_4 contains a QC over block_3 + auto block_4 = cluster.produce_and_push_block(); + cluster.process_node1_vote(); + auto block_4_fd = cluster.node0.node.control->head_finality_data(); + auto block_4_base_digest = block_4_fd.value().base_digest; + auto block_4_afp_base_digest = fc::sha256::hash(std::pair(active_finalizer_policy_digest, block_4_base_digest)); + + auto block_4_finality_root = block_4->action_mroot; + + // block_5 contains a QC over block_4, which completes the 3-chain for block_2 and serves as a proof of finality for it + auto block_5 = cluster.produce_and_push_block(); + cluster.process_node1_vote(); + auto block_5_fd = cluster.node0.node.control->head_finality_data(); + auto block_5_base_digest = block_5_fd.value().base_digest; + auto block_5_afp_base_digest = fc::sha256::hash(std::pair(active_finalizer_policy_digest, block_5_base_digest)); + auto block_5_finality_root = block_5->action_mroot; + + // retrieve the QC over block_4 that is contained in block_5 + qc_data_t qc_b_5 = extract_qc_data(block_5); + + BOOST_TEST(qc_b_5.qc.has_value()); + + // block_5 contains a QC over block_4, which completes the 3-chain for block_2 and serves as a proof of finality for it + auto block_6 = cluster.produce_and_push_block(); + cluster.process_node1_vote(); + + // retrieve the QC over block_5 that is contained in block_6 + qc_data_t qc_b_6 = extract_qc_data(block_6); + + BOOST_TEST(qc_b_6.qc.has_value()); + + std::string raw_bitset("03"); //node0 ande node1 signed + + // create a few proofs we'll use to perform tests + + // heavy proof #1. Proving finality of block #2 using block #2 finality root + mutable_variant_object heavy_proof_1 = mvo() + ("proof", mvo() + ("finality_proof", mvo() //proves finality of block #2 + ("qc_block", mvo() ("major_version", 1) ("minor_version", 0) ("finalizer_policy_generation", 1) - ("witness_hash", "888ceeb757ea240d1c1ae2f4f717e67b73dcd592b2ba097f63b4c3e3ca4350e1") - ("finality_mroot", "1d2ab7379301370d3fa1b27a9f4ac077f6ea445a1aa3dbf7e18e9cc2c25b140c") + ("witness_hash", block_4_afp_base_digest) + ("finality_mroot", block_4_finality_root) ) - ("qc", mutable_variant_object() - ("signature", "") - ("finalizers", fc::variants({})) + ("qc", mvo() + ("signature", qc_b_5.qc.value().data.sig.to_string()) + ("finalizers", raw_bitset) ) ) - ("target_block_proof_of_inclusion", mutable_variant_object() - ("target_node_index", 7) - ("last_node_index", 7) - ("target", fc::variants({"block_data", mutable_variant_object() - ("finality_data", mutable_variant_object() + ("target_block_proof_of_inclusion", mvo() + ("target_node_index", 2) + ("last_node_index", 2) + ("target", mvo() //target block #2 + ("finality_data", mvo() ("major_version", 1) ("minor_version", 0) ("finalizer_policy_generation", 1) - ("witness_hash", "dff620c1c4d31cade95ed609269a86d4ecb2357f9302d17675c0665c75786508") - ("finality_mroot", "1397eb7c86719f160188fa740fc3610ccb5a6681ad56807dc99a17fe73a7b7fd") + ("witness_hash", block_2_afp_base_digest) + ("finality_mroot", block_2_finality_root) ) - ("dynamic_data", mutable_variant_object() - ("block_num", 28) + ("dynamic_data", mvo() + ("block_num", block_2->block_num()) ("action_proofs", fc::variants()) - ("action_mroot", "4e890ef0e014f93bd1b31fabf1041ecc9fb1c44e957c2f7b1682333ee426677a") + ("action_mroot", block_2_action_mroot) ) - })) - ("merkle_branches", fc::variants({ - mutable_variant_object() - ("direction", 1) - ("hash", "4e17da018040c80339f2714828d1927d5b616f9af7aa4768c1876df6f05e5602") - , - mutable_variant_object() - ("direction", 1) - ("hash", "7ee0e16f1941fb5a98d80d20ca92e0c689e9284285d5f90ecd4f8f1ea2ffb53c") - , - mutable_variant_object() - ("direction", 1) - ("hash", "401526ba03ec4a955c83cda131dacd3e89becaad2cf04107170e436dd90a553f") - })) + ) + ("merkle_branches", generate_proof_of_inclusion({genesis_block_leaf, block_1_leaf, block_2_leaf}, 2)) ) - ) - ); - } -}; + ); + + // heavy proof #2. Proving finality of block #2 using block #3 finality root + mutable_variant_object heavy_proof_2 = mvo() + ("proof", mvo() + ("finality_proof", mvo() //proves finality of block #3 + ("qc_block", mvo() + ("major_version", 1) + ("minor_version", 0) + ("finalizer_policy_generation", 1) + ("witness_hash", block_5_afp_base_digest) + ("finality_mroot", block_5_finality_root) + ) + ("qc", mvo() + ("signature", qc_b_6.qc.value().data.sig.to_string()) + ("finalizers", raw_bitset) + ) + ) + ("target_block_proof_of_inclusion", mvo() + ("target_node_index", 2) + ("last_node_index", 3) + ("target", mvo() //target block #2 + ("finality_data", mvo() + ("major_version", 1) + ("minor_version", 0) + ("finalizer_policy_generation", 1) + ("witness_hash", block_2_afp_base_digest) + ("finality_mroot", block_2_finality_root) + ) + ("dynamic_data", mvo() + ("block_num", block_2->block_num()) + ("action_proofs", fc::variants()) + ("action_mroot", block_2_action_mroot) + ) + ) + ("merkle_branches", generate_proof_of_inclusion({genesis_block_leaf, block_1_leaf, block_2_leaf, block_3_leaf}, 2)) + ) + ); + + // light proof #1. Attempt to prove finality of block #2 with previously proven finality root of block #2 + mutable_variant_object light_proof_1 = mvo() + ("proof", mvo() + ("target_block_proof_of_inclusion", mvo() + ("target_node_index", 2) + ("last_node_index", 2) + ("target", mvo() + ("finality_data", mvo() + ("major_version", 1) + ("minor_version", 0) + ("finalizer_policy_generation", 1) + ("witness_hash", block_2_afp_base_digest) + ("finality_mroot", block_2_finality_root) + ) + ("dynamic_data", mvo() + ("block_num", block_2->block_num()) + ("action_proofs", fc::variants()) + ("action_mroot", block_2_action_mroot) + ) + ) + ("merkle_branches", generate_proof_of_inclusion({genesis_block_leaf, block_1_leaf, block_2_leaf}, 2)) + ) + ); + + + // verify first heavy proof + cluster.node0.node.push_action("ibc"_n, "checkproof"_n, "ibc"_n, heavy_proof_1); + + // now that we stored the proven root, we should be able to verify the same proof without the finality data (aka light proof) + cluster.node0.node.push_action("ibc"_n, "checkproof"_n, "ibc"_n, light_proof_1); + + // verify a second proof where the target block is different from the finality block. This also saves a second finality root to the contract, marking the beginning of the cache timer for the older finality root. + cluster.node0.node.push_action("ibc"_n, "checkproof"_n, "ibc"_n, heavy_proof_2); + + cluster.produce_blocks(1); //advance 1 block to avoid duplicate transaction + + // 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.node.push_action("ibc"_n, "checkproof"_n, "ibc"_n, light_proof_1); + + cluster.produce_blocks(1200); //advance 10 minutes + + // the root is still cached when performing this action, so the action succeeds. However, it also triggers garbage collection,removing the old proven root for block #2, so subsequent call with the same action data will fail + cluster.node0.node.push_action("ibc"_n, "checkproof"_n, "ibc"_n, light_proof_1); + + cluster.produce_blocks(1); //advance 1 block to avoid duplicate transaction -BOOST_AUTO_TEST_CASE( first_test ) try { - - ibc_tester chain_a; + bool failed = false; - chain_a.setup(); + // Since garbage collection was previously triggered for the merkle root of block #2 which this proof attempts to link to, action will now fail + try { + cluster.node0.node.push_action("ibc"_n, "checkproof"_n, "ibc"_n, light_proof_1); + } + catch(const eosio_assert_message_exception& e){ + failed = true; + } - chain_a.set_policy(); - chain_a.check_proof(); + // verify action has failed, as expected + BOOST_CHECK(failed); -} FC_LOG_AND_RETHROW() + } FC_LOG_AND_RETHROW() } BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/unittests/test-contracts/svnn_ibc/bitset.hpp b/unittests/test-contracts/svnn_ibc/bitset.hpp index 711e089fad..a9f83e3ca7 100644 --- a/unittests/test-contracts/svnn_ibc/bitset.hpp +++ b/unittests/test-contracts/svnn_ibc/bitset.hpp @@ -1,45 +1,49 @@ using namespace eosio; + class bitset { + using word_t = uint8_t; + static constexpr size_t num_bits = sizeof(word_t) * 8; + static size_t round_up(size_t sz) { return (sz + num_bits - 1) / num_bits; } + public: - bitset(size_t size) - : num_bits(size), data((size + 63) / 64) {} + bitset(word_t size) + : data(round_up(size), 0) {} - bitset(size_t size, const std::vector& raw_bitset) - : num_bits(size), data(raw_bitset) { - check(raw_bitset.size() == (size + 63) / 64, "invalid raw bitset size"); - + bitset(word_t size, const std::vector raw_bitset) + : data(raw_bitset) { + check(raw_bitset.size() == round_up(size), "invalid raw bitset size"); } // Set a bit to 1 - void set(size_t index) { + void set(word_t index) { check_bounds(index); - data[index / 64] |= (1ULL << (index % 64)); + data[index / 32] |= (1UL << (index % num_bits)); } // Clear a bit (set to 0) - void clear(size_t index) { + void clear(word_t index) { check_bounds(index); - data[index / 64] &= ~(1ULL << (index % 64)); + data[index / num_bits] &= ~(1UL << (index % num_bits)); } // Check if a bit is set - bool test(size_t index) const { + bool test(word_t index) const { check_bounds(index); - return (data[index / 64] & (1ULL << (index % 64))) != 0; + return (data[index / num_bits] & (1UL << (index % num_bits))) != 0; } // Size of the bitset - size_t size() const { + word_t size() const { return num_bits; } private: - size_t num_bits; - std::vector data; + std::vector data; // Check if the index is within bounds - void check_bounds(size_t index) const { + void check_bounds(word_t index) const { check(index < num_bits, "bitset index out of bounds"); } }; + diff --git a/unittests/test-contracts/svnn_ibc/svnn_ibc.abi b/unittests/test-contracts/svnn_ibc/svnn_ibc.abi index a6737df603..21b7653dc9 100644 --- a/unittests/test-contracts/svnn_ibc/svnn_ibc.abi +++ b/unittests/test-contracts/svnn_ibc/svnn_ibc.abi @@ -1,20 +1,7 @@ { "____comment": "This file was generated with eosio-abigen. DO NOT EDIT ", "version": "eosio::abi/1.2", - "types": [ - { - "new_type_name": "bls_public_key", - "type": "bytes" - }, - { - "new_type_name": "bls_signature", - "type": "bytes" - }, - { - "new_type_name": "target_data", - "type": "variant_block_data_action_data" - } - ], + "types": [], "structs": [ { "name": "action_data", @@ -34,6 +21,28 @@ } ] }, + { + "name": "action_proof_of_inclusion", + "base": "", + "fields": [ + { + "name": "target_node_index", + "type": "uint64" + }, + { + "name": "last_node_index", + "type": "uint64" + }, + { + "name": "target", + "type": "action_data" + }, + { + "name": "merkle_branches", + "type": "checksum256[]" + } + ] + }, { "name": "block_data", "base": "", @@ -79,19 +88,36 @@ ] }, { - "name": "checkproof", + "name": "block_proof_of_inclusion", "base": "", "fields": [ { - "name": "proof", - "type": "proof" + "name": "target_node_index", + "type": "uint64" + }, + { + "name": "last_node_index", + "type": "uint64" + }, + { + "name": "target", + "type": "block_data" + }, + { + "name": "merkle_branches", + "type": "checksum256[]" } ] }, { - "name": "clear", + "name": "checkproof", "base": "", - "fields": [] + "fields": [ + { + "name": "proof", + "type": "proof" + } + ] }, { "name": "dynamic_data_v0", @@ -103,7 +129,7 @@ }, { "name": "action_proofs", - "type": "proof_of_inclusion[]" + "type": "action_proof_of_inclusion[]" }, { "name": "action_mroot", @@ -134,12 +160,12 @@ "type": "string" }, { - "name": "fweight", + "name": "weight", "type": "uint64" }, { "name": "public_key", - "type": "bls_public_key" + "type": "string" } ] }, @@ -152,7 +178,7 @@ "type": "uint32" }, { - "name": "fthreshold", + "name": "threshold", "type": "uint64" }, { @@ -165,10 +191,6 @@ "name": "lastproof", "base": "", "fields": [ - { - "name": "id", - "type": "uint64" - }, { "name": "block_num", "type": "uint32" @@ -183,20 +205,6 @@ } ] }, - { - "name": "merkle_branch", - "base": "", - "fields": [ - { - "name": "direction", - "type": "uint8" - }, - { - "name": "hash", - "type": "checksum256" - } - ] - }, { "name": "permission_level", "base": "", @@ -221,29 +229,7 @@ }, { "name": "target_block_proof_of_inclusion", - "type": "proof_of_inclusion" - } - ] - }, - { - "name": "proof_of_inclusion", - "base": "", - "fields": [ - { - "name": "target_node_index", - "type": "uint64" - }, - { - "name": "last_node_index", - "type": "uint64" - }, - { - "name": "target", - "type": "target_data" - }, - { - "name": "merkle_branches", - "type": "merkle_branch[]" + "type": "block_proof_of_inclusion" } ] }, @@ -253,11 +239,11 @@ "fields": [ { "name": "finalizers", - "type": "uint64[]" + "type": "bytes" }, { "name": "signature", - "type": "bls_signature" + "type": "string" } ] }, @@ -328,11 +314,6 @@ "type": "checkproof", "ricardian_contract": "" }, - { - "name": "clear", - "type": "clear", - "ricardian_contract": "" - }, { "name": "setfpolicy", "type": "setfpolicy", @@ -356,11 +337,6 @@ } ], "ricardian_clauses": [], - "variants": [ - { - "name": "variant_block_data_action_data", - "types": ["block_data","action_data"] - } - ], + "variants": [], "action_results": [] } \ No newline at end of file diff --git a/unittests/test-contracts/svnn_ibc/svnn_ibc.cpp b/unittests/test-contracts/svnn_ibc/svnn_ibc.cpp index a5ec47f869..717ffa4a40 100644 --- a/unittests/test-contracts/svnn_ibc/svnn_ibc.cpp +++ b/unittests/test-contracts/svnn_ibc/svnn_ibc.cpp @@ -1,13 +1,10 @@ #include "svnn_ibc.hpp" //add two numbers from the g1 group (aggregation) -std::vector svnn_ibc::_g1add(const std::vector& op1, const std::vector& op2) { - check(op1.size() == std::tuple_size::value, "wrong op1 size passed"); - check(op2.size() == std::tuple_size::value, "wrong op2 size passed"); +bls_g1 svnn_ibc::_g1add(const bls_g1& op1, const bls_g1& op2) { bls_g1 r; - bls_g1_add(*reinterpret_cast(op1.data()), *reinterpret_cast(op2.data()), r); - std::vector v(r.begin(), r.end()); - return v; + bls_g1_add(op1, op2, r); + return r; } void svnn_ibc::_maybe_set_finalizer_policy(const fpolicy& policy, const uint32_t from_block_num){ @@ -26,7 +23,7 @@ void svnn_ibc::_maybe_set_finalizer_policy(const fpolicy& policy, const uint32_t } svnn_ibc::storedpolicy spolicy; spolicy.generation = policy.generation; - spolicy.fthreshold = policy.fthreshold; + spolicy.threshold = policy.threshold; spolicy.finalizers = policy.finalizers; //policy is in force until a newer policy is proven @@ -42,16 +39,16 @@ void svnn_ibc::_maybe_set_finalizer_policy(const fpolicy& policy, const uint32_t //adds the newly proven root if necessary void svnn_ibc::_maybe_add_proven_root(const uint32_t block_num, const checksum256& finality_mroot){ proofs_table _proofs_table(get_self(), get_self().value); - auto block_num_index = _proofs_table.get_index<"blocknum"_n>(); + //auto block_num_index = _proofs_table.get_index<"blocknum"_n>(); auto merkle_index = _proofs_table.get_index<"merkleroot"_n>(); - auto last_itr = block_num_index.rbegin(); + auto last_itr = _proofs_table.rbegin(); //if first proven root or newer than the last proven root, we store it - if (last_itr == block_num_index.rend() || last_itr->block_numblock_num<(uint64_t)block_num){ auto itr = merkle_index.find(finality_mroot); if (itr == merkle_index.end()){ _proofs_table.emplace( get_self(), [&]( auto& p ) { - p.id = _proofs_table.available_primary_key(); + //p.id = _proofs_table.available_primary_key(); p.block_num = block_num; p.finality_mroot = finality_mroot; p.cache_expiry = add_time(current_time_point(), PROOF_CACHE_EXPIRY); //set cache expiry @@ -61,29 +58,35 @@ void svnn_ibc::_maybe_add_proven_root(const uint32_t block_num, const checksum25 //otherwise, the proven root is not advancing finality so we don't need to store it } -//delete old policies and proofs that are no longer necessary -void svnn_ibc::_garbage_collection(){ - //todo : implement +template +void svnn_ibc::_maybe_remove_from_cache(){ + + time_point now = current_time_point(); + + Table table(get_self(), get_self().value); + + auto idx = table.template get_index<"expiry"_n>(); //expiry order index + + auto last_itr = idx.rbegin(); //last entry + + if (last_itr == idx.rend() ) return; //no entries, nothing to do + + if (now.sec_since_epoch() < last_itr->cache_expiry.sec_since_epoch()) return; //cache has not yet expired, nothing to do + else { + + //cache must be cleaned up + auto itr = idx.begin(); + while (itr!=idx.end()){ + if (itr->primary_key() == last_itr->primary_key()) return; //last entry, we always keep that one + itr = idx.erase(itr); + } + } + } //verify that a signature over a given message has been generated with the private key matching the public key -void svnn_ibc::_verify(const std::vector& pk, const std::vector& sig, std::vector& msg){ - check(pk.size() == std::tuple_size::value, "wrong pk size passed"); - check(sig.size() == std::tuple_size::value, "wrong sig size passed"); - bls_g1 g1_points[2]; - bls_g2 g2_points[2]; - - memcpy(g1_points[0].data(), eosio::detail::G1_ONE_NEG.data(), sizeof(bls_g1)); - memcpy(g2_points[0].data(), sig.data(), sizeof(bls_g2)); - - bls_g2 p_msg; - eosio::detail::g2_fromMessage(msg, eosio::detail::CIPHERSUITE_ID, p_msg); - memcpy(g1_points[1].data(), pk.data(), sizeof(bls_g1)); - memcpy(g2_points[1].data(), p_msg.data(), sizeof(bls_g2)); - - bls_gt r; - bls_pairing(g1_points, g2_points, 2, r); - check(0 == memcmp(r.data(), eosio::detail::GT_ONE.data(), sizeof(bls_gt)), "bls signature verify failed"); +void svnn_ibc::_verify(const std::string& public_key, const std::string& signature, const std::string& message){ + check(bls_signature_verify(decode_bls_public_key_to_g1(public_key), decode_bls_signature_to_g2(signature), message), "signature verify failed"); } //verify that the quorum certificate over the finality digest is valid @@ -99,42 +102,40 @@ void svnn_ibc::_check_qc(const quorum_certificate& qc, const checksum256& finali auto fa_itr = target_policy.finalizers.begin(); auto fa_end_itr = target_policy.finalizers.end(); size_t finalizer_count = std::distance(fa_itr, fa_end_itr); - std::vector bitset_data(qc.finalizers); - bitset b(finalizer_count, bitset_data); + bitset b(finalizer_count, qc.finalizers); bool first = true; size_t index = 0; uint64_t weight = 0; - bls_public_key agg_pub_key; + 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 = fa_itr->public_key; + agg_pub_key = pub_key; } - else agg_pub_key = _g1add(agg_pub_key, fa_itr->public_key); - weight+=fa_itr->fweight; - + else agg_pub_key = _g1add(agg_pub_key, pub_key); + weight+=fa_itr->weight; } index++; fa_itr++; } //verify that we have enough vote weight to meet the quorum threshold of the target policy - check(weight>=target_policy.fthreshold, "insufficient signatures to reach quorum"); - std::array data = finality_digest.extract_as_byte_array(); - std::vector v_data(data.begin(), data.end()); + check(weight>=target_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()); + + std::string s_agg_pub_key = encode_g1_to_bls_public_key(agg_pub_key); //verify signature validity - _verify(agg_pub_key, qc.signature, v_data); + _verify(s_agg_pub_key, qc.signature, message); } -void svnn_ibc::_check_target_block_proof_of_inclusion(const proof_of_inclusion& proof, const std::optional reference_root){ - - //verify that the proof of inclusion is over a target block - check(std::holds_alternative(proof.target), "must supply proof of inclusion over block data"); +void svnn_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(); @@ -147,25 +148,23 @@ void svnn_ibc::_check_target_block_proof_of_inclusion(const proof_of_inclusion& auto itr = merkle_index.find(finality_mroot); check(itr!= merkle_index.end(), "cannot link proof to proven merkle root"); } - block_data target_block = std::get(proof.target); - if (target_block.finality_data.active_finalizer_policy.has_value()){ - _maybe_set_finalizer_policy(target_block.finality_data.active_finalizer_policy.value(), target_block.dynamic_data.block_num); + //block_data target_block = std::get(proof.target); + if (proof.target.finality_data.active_finalizer_policy.has_value()){ + _maybe_set_finalizer_policy(proof.target.finality_data.active_finalizer_policy.value(), proof.target.dynamic_data.block_num); } } -void svnn_ibc::_check_finality_proof(const finality_proof& finality_proof, const proof_of_inclusion& target_block_proof_of_inclusion){ - //temporarilly disabled : skip qc verification +void svnn_ibc::_check_finality_proof(const finality_proof& finality_proof, const block_proof_of_inclusion& target_block_proof_of_inclusion){ + //if QC is valid, it means that we have reaced finality on the block referenced by the finality_mroot - //_check_qc(finality_proof.qc, finality_proof.qc_block.finality_digest(), finality_proof.qc_block.finalizer_policy_generation); + _check_qc(finality_proof.qc, finality_proof.qc_block.finality_digest(), finality_proof.qc_block.finalizer_policy_generation); //check if the target proof of inclusion correctly resolves to the root of the finality proof _check_target_block_proof_of_inclusion(target_block_proof_of_inclusion, finality_proof.qc_block.finality_mroot); - //if proof of inclusion was successful, the target block and its dynamic data have been validated as final and correct - block_data target_block = std::get(target_block_proof_of_inclusion.target); - //if the finality_mroot we just proven is more recent than the last root we have stored, store it - _maybe_add_proven_root(target_block.dynamic_data.block_num, finality_proof.qc_block.finality_mroot); + uint64_t offset = target_block_proof_of_inclusion.last_node_index - target_block_proof_of_inclusion.target_node_index; + _maybe_add_proven_root(target_block_proof_of_inclusion.target.dynamic_data.block_num + offset, finality_proof.qc_block.finality_mroot); } ACTION svnn_ibc::setfpolicy(const fpolicy& policy, const uint32_t from_block_num){ @@ -179,6 +178,10 @@ ACTION svnn_ibc::setfpolicy(const fpolicy& policy, const uint32_t from_block_num check(_policies_table.begin() == _policies_table.end(), "can only set finalizer policy manually for initialization"); _maybe_set_finalizer_policy(policy, from_block_num); + + //clean up if necessary + _maybe_remove_from_cache(); + _maybe_remove_from_cache(); } ACTION svnn_ibc::checkproof(const proof& proof){ @@ -191,19 +194,9 @@ ACTION svnn_ibc::checkproof(const proof& proof){ //if we only have a proof of inclusion of the target block, we execute the "light" code path _check_target_block_proof_of_inclusion(proof.target_block_proof_of_inclusion, std::nullopt); } -} -//temporary : reset the state -ACTION svnn_ibc::clear(){ - require_auth(get_self()); - proofs_table _proofs_table(get_self(), get_self().value); - policies_table _policies_table(get_self(), get_self().value); - auto fp_itr = _proofs_table.begin(); - while (fp_itr!= _proofs_table.end()){ - fp_itr = _proofs_table.erase(fp_itr); - } - auto p_itr = _policies_table.begin(); - while (p_itr!= _policies_table.end()){ - p_itr = _policies_table.erase(p_itr); - } -} \ No newline at end of file + //clean up if necessary + _maybe_remove_from_cache(); + _maybe_remove_from_cache(); + +} diff --git a/unittests/test-contracts/svnn_ibc/svnn_ibc.hpp b/unittests/test-contracts/svnn_ibc/svnn_ibc.hpp index 6ac6d8597b..825e73eb6a 100644 --- a/unittests/test-contracts/svnn_ibc/svnn_ibc.hpp +++ b/unittests/test-contracts/svnn_ibc/svnn_ibc.hpp @@ -15,14 +15,11 @@ CONTRACT svnn_ibc : public contract { public: using contract::contract; - using bls_public_key = std::vector; - using bls_signature = std::vector; - const uint32_t POLICY_CACHE_EXPIRY = 600; //10 minutes for testing const uint32_t PROOF_CACHE_EXPIRY = 600; //10 minutes for testing //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) { + static uint64_t calculate_max_depth(uint64_t node_count) { if(node_count <= 1) return node_count; return 64 - __builtin_clzll(2 << (64 - 1 - __builtin_clzll ((node_count - 1)))); @@ -50,46 +47,43 @@ CONTRACT svnn_ibc : public contract { return tp; } - /* + //compute proof path + static std::vector _get_proof_path(const uint64_t c_leaf_index, uint64_t const c_leaf_count) { - //discuss : compute merkle branch direction vs providing them as part of the proof + uint64_t leaf_index = c_leaf_index; + uint64_t leaf_count = c_leaf_count; - static std::vector _get_directions(const uint64_t index, const uint64_t last_node_index){ + std::vector proof_path; - std::vector proof; + uint64_t layers_depth = calculate_max_depth(c_leaf_count) -1; - uint64_t c_index = index; - uint64_t layers_depth = calculate_max_depth(last_node_index) -1; - uint64_t c_last_node_index = last_node_index; + for (uint64_t i = 0; i < layers_depth; i++) { + bool isLeft = leaf_index % 2; + uint64_t pairIndex = isLeft ? leaf_index - 1 : + (leaf_index == leaf_count ? leaf_index : leaf_index + 1); - for (uint64_t i = 0; i < layers_depth; i++) { - if (c_last_node_index % 2) c_last_node_index+=1; - bool isLeft = c_index % 2 == 0 ? 0 : 1; - uint64_t pairIndex = isLeft ? c_index - 1 : - c_index == last_node_index - 1 && i < layers_depth - 1 ? c_index : - c_index + 1; - c_last_node_index/=2; - if (pairIndex < last_node_index) proof.push_back(isLeft); - c_index = c_index / 2; - } - return proof; - } - - */ + + if (pairIndex proof_nodes, const checksum256& target){ + static checksum256 _compute_root(const std::vector proof_nodes, const checksum256& target, const uint64_t target_index, const uint64_t last_node_index){ checksum256 hash = target; + std::vector proof_path = _get_proof_path(target_index, last_node_index+1); + + check(proof_path.size() == proof_nodes.size(), "internal error"); //should not happen for (int i = 0 ; i < proof_nodes.size() ; i++){ - const checksum256 node = proof_nodes[i].hash; + + const checksum256 node = proof_nodes[i]; std::array arr = node.extract_as_byte_array(); - if (proof_nodes[i].direction == 0){ + if (proof_path[i] == false){ hash = hash_pair(std::make_pair(hash, node)); } else { @@ -100,20 +94,20 @@ CONTRACT svnn_ibc : public contract { } struct quorum_certificate { - std::vector finalizers; - bls_signature signature; + std::vector finalizers; + std::string signature; }; struct finalizer_authority { - std::string description; - uint64_t fweight = 0; - bls_public_key public_key; + std::string description; + uint64_t weight = 0; + std::string public_key; }; struct fpolicy { uint32_t generation = 0; ///< sequentially incrementing version number - uint64_t fthreshold = 0; ///< vote fweight threshold to finalize blocks + uint64_t threshold = 0; ///< vote weight threshold to finalize blocks std::vector finalizers; ///< Instant Finality voter set checksum256 digest() const { @@ -133,22 +127,19 @@ CONTRACT svnn_ibc : public contract { uint64_t primary_key() const {return generation;} uint64_t by_cache_expiry()const { return cache_expiry.sec_since_epoch(); } - EOSLIB_SERIALIZE( storedpolicy, (generation)(fthreshold)(finalizers)(last_block_num)(cache_expiry)) + EOSLIB_SERIALIZE( storedpolicy, (generation)(threshold)(finalizers)(last_block_num)(cache_expiry)) }; TABLE lastproof { - uint64_t id; - uint32_t block_num; checksum256 finality_mroot; time_point cache_expiry; - uint64_t primary_key()const { return id; } - uint64_t by_block_num()const { return block_num; } + uint64_t primary_key()const { return (uint64_t)block_num; } uint64_t by_cache_expiry()const { return cache_expiry.sec_since_epoch(); } checksum256 by_merkle_root()const { return finality_mroot; } @@ -220,16 +211,55 @@ CONTRACT svnn_ibc : public contract { }; - struct proof_of_inclusion; + //struct proof_of_inclusion; + + struct action_data { + + r_action action; //antelope action + checksum256 action_receipt_digest; //required witness hash, actual action_receipt is irrelevant to IBC + + std::vector return_value; //empty if no return value + + //returns the action digest + checksum256 action_digest() const { + return action.digest(); + }; + + //returns the receipt digest, composed of the action_digest() and action_receipt_digest witness hash + checksum256 digest() const { + checksum256 action_receipt_digest = hash_pair( std::make_pair( action_digest(), action_receipt_digest) ); + return action_receipt_digest; + }; + + }; + + struct action_proof_of_inclusion { + + uint64_t target_node_index; + uint64_t last_node_index; + + action_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_node_index, last_node_index); + return root; + }; + + }; struct dynamic_data_v0 { //block_num is always present uint32_t block_num; + //can include any number of action_proofs and / or state_proofs pertaining to a given block //all action_proofs must resolve to the same action_mroot - std::vector action_proofs; + std::vector action_proofs; //can be used instead of providing action_proofs. Useful for proving finalizer policy changes std::optional action_mroot; @@ -322,43 +352,19 @@ CONTRACT svnn_ibc : public contract { }; }; - struct action_data { - - r_action action; //antelope action - checksum256 action_receipt_digest; //required witness hash, actual action_receipt is irrelevant to IBC - - std::vector return_value; //empty if no return value - - //returns the action digest - checksum256 action_digest() const { - return action.digest(); - }; - - //returns the receipt digest, composed of the action_digest() and action_receipt_digest witness hash - checksum256 digest() const { - checksum256 action_receipt_digest = hash_pair( std::make_pair( action_digest(), action_receipt_digest) ); - return action_receipt_digest; - }; - - }; - - using target_data = std::variant; - - - struct proof_of_inclusion { + struct block_proof_of_inclusion { uint64_t target_node_index; uint64_t last_node_index; - target_data target; + block_data target; - std::vector merkle_branches; + std::vector merkle_branches; //returns the merkle root obtained by hashing target.digest() with merkle_branches checksum256 root() const { - auto call_digest = [](const auto& var) -> checksum256 { return var.digest(); }; - checksum256 digest = std::visit(call_digest, target); - checksum256 root = _compute_root(merkle_branches, digest); + checksum256 digest = target.digest(); + checksum256 root = _compute_root(merkle_branches, digest, target_node_index, last_node_index); return root; }; @@ -380,7 +386,7 @@ CONTRACT svnn_ibc : public contract { //1) finality_proof for a QC block, and proof_of_inclusion of a target block within the final_on_strong_qc block represented by the finality_mroot present in header //2) only a proof_of_inclusion of a target block, which must be included in a merkle tree represented by a root stored in the contract's RAM std::optional finality_proof; - proof_of_inclusion target_block_proof_of_inclusion; + block_proof_of_inclusion target_block_proof_of_inclusion; }; @@ -388,27 +394,24 @@ CONTRACT svnn_ibc : public contract { indexed_by<"expiry"_n, const_mem_fun>> policies_table; typedef eosio::multi_index< "lastproofs"_n, lastproof, - indexed_by<"blocknum"_n, const_mem_fun>, indexed_by<"merkleroot"_n, const_mem_fun>, indexed_by<"expiry"_n, const_mem_fun>> proofs_table; - std::vector _g1add(const std::vector& op1, const std::vector& op2); + bls_g1 _g1add(const bls_g1& op1, const bls_g1& op2); void _maybe_set_finalizer_policy(const fpolicy& policy, const uint32_t from_block_num); void _maybe_add_proven_root(const uint32_t block_num, const checksum256& finality_mroot); - void _garbage_collection(); + template + void _maybe_remove_from_cache(); - void _verify(const std::vector& pk, const std::vector& sig, std::vector& msg); + void _verify(const std::string& public_key, const std::string& signature, const std::string& message); void _check_qc(const quorum_certificate& qc, const checksum256& finality_mroot, const uint64_t finalizer_policy_generation); - void _check_finality_proof(const finality_proof& finality_proof, const proof_of_inclusion& target_block_proof_of_inclusion); - void _check_target_block_proof_of_inclusion(const proof_of_inclusion& proof, const std::optional reference_root); + 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); ACTION setfpolicy(const fpolicy& policy, const uint32_t from_block_num); //set finality policy ACTION checkproof(const proof& proof); - //clearing function, to be removed for production version - ACTION clear(); - }; \ No newline at end of file diff --git a/unittests/test-contracts/svnn_ibc/svnn_ibc.wasm b/unittests/test-contracts/svnn_ibc/svnn_ibc.wasm index 8f5d1276fd..7e8d2fd799 100755 Binary files a/unittests/test-contracts/svnn_ibc/svnn_ibc.wasm and b/unittests/test-contracts/svnn_ibc/svnn_ibc.wasm differ